diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4582ffa74..918a2018f7 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-22.04, + os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-x86_64, } diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e7e268a5..be3a1e8b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,49 +1,4 @@ # 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 -- Supported nested lists within callout and quote blocks -- Revamped the document's floating toolbar and added Turn Into -- Enabled custom icons in callout blocks -### Bug Fixes -- Fixed occasional incorrect positioning of the slash menu -- Improved AI Chat and AI Writers with various bug fixes -- Adjusted the columns block to match the width of the editor -- Fixed a potential segfault caused by infinite recursion in the trash view -- Resolved an issue where the first added cover might be invisible -- Fixed adding cover images via Unsplash - ## Version 0.8.6 - 06/03/2025 ### Bug Fixes - Fix the incorrect title positioning when adjusting the document width setting diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 41fdffb1af..89ee36ad6a 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.8.9" +APPFLOWY_VERSION = "0.8.6" 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 4579b2d8c5..8da401ef26 100644 --- a/frontend/appflowy_flutter/analysis_options.yaml +++ b/frontend/appflowy_flutter/analysis_options.yaml @@ -4,7 +4,6 @@ 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 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf new file mode 100644 index 0000000000..8f03a5c8f9 Binary files /dev/null and b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf differ diff --git a/frontend/appflowy_flutter/assets/translations/mr-IN.json b/frontend/appflowy_flutter/assets/translations/mr-IN.json deleted file mode 100644 index f86a1e0081..0000000000 --- a/frontend/appflowy_flutter/assets/translations/mr-IN.json +++ /dev/null @@ -1,3210 +0,0 @@ -{ - "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/dsa_priv.pem b/frontend/appflowy_flutter/dsa_priv.pem new file mode 100644 index 0000000000..66843054b7 --- /dev/null +++ b/frontend/appflowy_flutter/dsa_priv.pem @@ -0,0 +1,26 @@ +-----BEGIN PRIVATE KEY----- +MIIEXAIBADCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0Y +ruaTrrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7t +J8mG4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy +7xyw+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL +7iTVKiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqH +Opf5b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdp +qm4ZQRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFy +JiJWYWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ +5EhGG4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAh +Lswu6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPh +AsVA6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxC +xMTpq1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIA +Pxbd0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+V +uix/4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/ +8WrbK13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqT +QJg7hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19 +tKcOs6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQd +bsCzAxp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzf +J4v4uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6T +jcfVWthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NCl +WgZnixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3w +m7NB+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcT +ilaNC9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkEHgIcfy0+ZHp+4MBcWSDv +uzWeM8QmNvbP+owM+H4F7A== +-----END PRIVATE KEY----- diff --git a/frontend/appflowy_flutter/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 e34ac02aab..230ee59495 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart @@ -15,6 +15,7 @@ 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 0b77a0167b..3271070c74 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_cell.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.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(SearchResultCell), findsNWidgets(2)); + expect(find.byType(SearchResultTile), findsNWidgets(2)); // The score should be higher for "ViewOna" thus it should be shown first final secondDocumentWidget = tester - .widget(find.byType(SearchResultCell).first) as SearchResultCell; - expect(secondDocumentWidget.item.displayName, secondDocument); + .widget(find.byType(SearchResultTile).first) as SearchResultTile; + expect(secondDocumentWidget.result.data, 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(SearchResultCell).first, - ) as SearchResultCell; - expect(firstDocumentWidget.item.displayName, firstDocument); + find.byType(SearchResultTile).first, + ) as SearchResultTile; + expect(firstDocumentWidget.result.data, 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(SearchResultCell), findsNWidgets(2)); + expect(find.byType(SearchResultTile), findsNWidgets(2)); /// check results final svgs = find.descendant( - of: find.byType(SearchResultCell), + of: find.byType(SearchResultTile), 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 b9495ae0e7..277ae8f21e 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/search_recent_view_cell.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.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,12 +27,11 @@ void main() { expect(find.byType(RecentViewsList), findsOneWidget); // Expect three recent history items - expect(find.byType(SearchRecentViewCell), findsNWidgets(3)); + expect(find.byType(RecentViewTile), findsNWidgets(3)); // Expect the first item to be the last viewed document final firstDocumentWidget = - tester.widget(find.byType(SearchRecentViewCell).first) - as SearchRecentViewCell; + tester.widget(find.byType(RecentViewTile).first) as RecentViewTile; 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 a71110f1e0..9b9434d3d7 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,7 +15,6 @@ 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); @@ -30,11 +29,6 @@ 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'); @@ -66,40 +60,5 @@ 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 71656c1ea6..e35c9cc9d8 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,10 +1,5 @@ -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'; @@ -78,37 +73,5 @@ 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_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index 1a8a3fcda8..ea8db6fdad 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -27,9 +27,8 @@ void main() { await tester.pumpAndSettle(); // click the align center - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); // expect to see the align center final editorState = tester.editor.getCurrentEditorState(); @@ -37,15 +36,13 @@ void main() { expect(first.attributes[blockComponentAlign], 'center'); // click the align right - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); expect(first.attributes[blockComponentAlign], 'right'); // click the align left - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); expect(first.attributes[blockComponentAlign], 'left'); }); 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 d1e34edcb5..c18b42939c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -1,12 +1,10 @@ -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'; @@ -322,14 +320,8 @@ 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, 1); + expect(editorState.document.root.children.length, 2); final node = editorState.getNodeAtPath([0])!; expect(node.type, LinkPreviewBlockKeys.type); expect(node.attributes[LinkPreviewBlockKeys.url], url); @@ -341,20 +333,19 @@ void main() { await tester.hoverOnWidget( find.byType(CustomLinkPreviewWidget), onHover: () async { - /// 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(), - ); + final convertToLinkButton = find.byWidgetPredicate((widget) { + return widget is MenuBlockButton && + widget.tooltip == + LocaleKeys.document_plugins_urlPreview_convertToLink.tr(); + }); expect(convertToLinkButton, findsOneWidget); - await tester.tapButton(convertToLinkButton); + await tester.tap(convertToLinkButton); + await tester.pumpAndSettle(); }, ); + await tester.pumpAndSettle(); + final editorState = tester.editor.getCurrentEditorState(); final textNode = editorState.getNodeAtPath([0])!; expect(textNode.type, ParagraphBlockKeys.type); @@ -372,19 +363,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, 1); + expect(editorState.document.root.children.length, 2); 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: @@ -483,6 +469,16 @@ 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 @@ -525,7 +521,7 @@ void main() { extension on WidgetTester { Future pasteContent( - FutureOr Function(EditorState editorState) test, { + void Function(EditorState editorState) test, { Future Function(EditorState editorState)? beforeTest, String? plainText, String? html, @@ -562,6 +558,6 @@ extension on WidgetTester { ); await pumpAndSettle(const Duration(milliseconds: 1000)); - await test(editor.getCurrentEditorState()); + 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 c2e00a4b48..43320509ce 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,8 +13,6 @@ 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 deleted file mode 100644 index 39f8bfd4f6..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart +++ /dev/null @@ -1,453 +0,0 @@ -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_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index 6ec12287a8..cbc634cf02 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -76,12 +76,13 @@ void main() { LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.editor_bulletedListShortForm.tr(): + LocaleKeys.document_slashMenu_name_bulletedList.tr(): BulletedListBlockKeys.type, - LocaleKeys.editor_numberedListShortForm.tr(): + LocaleKeys.document_slashMenu_name_numberedList.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, + LocaleKeys.document_slashMenu_name_todoList.tr(): + TodoListBlockKeys.type, LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, }; @@ -116,12 +117,13 @@ void main() { LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.editor_bulletedListShortForm.tr(): + LocaleKeys.document_slashMenu_name_bulletedList.tr(): BulletedListBlockKeys.type, - LocaleKeys.editor_numberedListShortForm.tr(): + LocaleKeys.document_slashMenu_name_numberedList.tr(): NumberedListBlockKeys.type, LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, + LocaleKeys.document_slashMenu_name_todoList.tr(): + TodoListBlockKeys.type, LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, }; diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart index de1cb880a5..bd0fd18c50 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,6 +1,5 @@ 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'; @@ -48,41 +47,5 @@ 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 bc0671834b..a05545753e 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,7 +13,6 @@ 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(); @@ -29,5 +28,4 @@ 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 f455cd479d..c3a086626f 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,19 +1,5 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/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'; @@ -22,33 +8,24 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - Future selectText(WidgetTester tester, String text) async { - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text.length, - ), - ); - } - - Future prepareForToolbar(WidgetTester tester, String text) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(text); - await selectText(tester, text); - } - group('document toolbar:', () { testWidgets('font family', (tester) async { - await prepareForToolbar(tester, 'font family'); + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(); + + await tester.editor.tapLineOfEditorAt(0); + const text = 'font family'; + await tester.ime.insertText(text); + await tester.editor.updateSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: text.length, + ), + ); - // tap more options button - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m); // tap the font family button final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey); await tester.tapButton(fontFamilyButton); @@ -69,302 +46,5 @@ void main() { abel, ); }); - - testWidgets('heading 1~3', (tester) async { - const text = 'heading'; - await prepareForToolbar(tester, text); - - Future testChangeHeading( - FlowySvgData svg, - String title, - int level, - ) async { - /// tap suggestions item - final suggestionsButton = find.byKey(kSuggestionsItemKey); - await tester.tapButton(suggestionsButton); - - /// tap item - await tester.ensureVisible(find.byFlowySvg(svg)); - await tester.tapButton(find.byFlowySvg(svg)); - - /// check the type of node is [HeadingBlockKeys.type] - await selectText(tester, text); - final editorState = tester.editor.getCurrentEditorState(); - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!, - nodeLevel = node.attributes[HeadingBlockKeys.level]!; - expect(node.type, HeadingBlockKeys.type); - expect(nodeLevel, level); - - /// show toolbar again - await selectText(tester, text); - - /// the text of suggestions item should be changed - expect( - find.descendant(of: suggestionsButton, matching: find.text(title)), - findsOneWidget, - ); - } - - await testChangeHeading( - FlowySvgs.type_h1_m, - LocaleKeys.document_toolbar_h1.tr(), - 1, - ); - - await testChangeHeading( - FlowySvgs.type_h2_m, - LocaleKeys.document_toolbar_h2.tr(), - 2, - ); - await testChangeHeading( - FlowySvgs.type_h3_m, - LocaleKeys.document_toolbar_h3.tr(), - 3, - ); - }); - - testWidgets('toggle 1~3', (tester) async { - const text = 'toggle'; - await prepareForToolbar(tester, text); - - Future testChangeToggle( - FlowySvgData svg, - String title, - int? level, - ) async { - /// tap suggestions item - final suggestionsButton = find.byKey(kSuggestionsItemKey); - await tester.tapButton(suggestionsButton); - - /// tap item - await tester.ensureVisible(find.byFlowySvg(svg)); - await tester.tapButton(find.byFlowySvg(svg)); - - /// check the type of node is [HeadingBlockKeys.type] - await selectText(tester, text); - final editorState = tester.editor.getCurrentEditorState(); - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!, - nodeLevel = node.attributes[ToggleListBlockKeys.level]; - expect(node.type, ToggleListBlockKeys.type); - expect(nodeLevel, level); - - /// show toolbar again - await selectText(tester, text); - - /// the text of suggestions item should be changed - expect( - find.descendant(of: suggestionsButton, matching: find.text(title)), - findsOneWidget, - ); - } - - await testChangeToggle( - FlowySvgs.type_toggle_list_m, - LocaleKeys.editor_toggleListShortForm.tr(), - null, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h1_m, - LocaleKeys.editor_toggleHeading1ShortForm.tr(), - 1, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h2_m, - LocaleKeys.editor_toggleHeading2ShortForm.tr(), - 2, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h3_m, - LocaleKeys.editor_toggleHeading3ShortForm.tr(), - 3, - ); - }); - - 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/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart index 67e0149cd1..906b5ab69c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -34,15 +33,9 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - // tap the inline math equation button - final inlineMathEquationButton = find.text( - LocaleKeys.document_toolbar_equation.tr(), + final inlineMathEquationButton = find.findFlowyTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), ); await tester.tapButton(inlineMathEquationButton); @@ -85,15 +78,10 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - // tap the inline math equation button - final inlineMathEquationButton = - find.byFlowySvg(FlowySvgs.type_formula_m); + var inlineMathEquationButton = find.findFlowyTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), + ); await tester.tapButton(inlineMathEquationButton); // expect to see the math equation block @@ -105,7 +93,17 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: 1), ); - await tester.tapButton(moreOptionButton); + // expect to the see the inline math equation button is highlighted + inlineMathEquationButton = find.descendant( + of: find.findFlowyTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), + ), + matching: find.byType(SVGIconItemWidget), + ); + expect( + tester.widget(inlineMathEquationButton).isHighlight, + isTrue, + ); // cancel the format await tester.tapButton(inlineMathEquationButton); @@ -136,15 +134,10 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - // tap the inline math equation button - final inlineMathEquationButton = - find.byFlowySvg(FlowySvgs.type_formula_m); + final inlineMathEquationButton = find.findFlowyTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), + ); await tester.tapButton(inlineMathEquationButton); // expect to see the math equation block diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart index c4aa289855..8eb47fc15f 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -86,10 +85,16 @@ void main() { ), ); - await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m)); + await tester.tapButton(find.byType(HeadingPopup)); + await tester.pumpAndSettle(); + + expect( + find.byType(HeadingButton), + findsNWidgets(3), + ); // tap the H1 button - await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0)); + await tester.tapButton(find.byType(HeadingButton).at(0)); await tester.pumpAndSettle(); final editorState = tester.editor.getCurrentEditorState(); 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 d3226a3ad0..554a6eecbf 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,166 +1,42 @@ import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/emoji/emoji_handler.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + +import '../../shared/keyboard.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - Future prepare(WidgetTester tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(); - await tester.editor.tapLineOfEditorAt(0); - } - // May be better to move this to an existing test but unsure what it fits with group('Keyboard shortcuts related to emojis', () { testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker', (tester) async { - await prepare(tester); + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); - expect(find.byType(EmojiHandler), findsNothing); + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyE, - isAltPressed: true, - isMetaPressed: Platform.isMacOS, - isControlPressed: !Platform.isMacOS, + expect(find.byType(EmojiSelectionMenu), findsNothing); + + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + Platform.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.alt, + LogicalKeyboardKey.keyE, + ], + tester: tester, ); - await tester.pumpAndSettle(Duration(seconds: 1)); - expect(find.byType(EmojiHandler), findsOneWidget); - /// press backspace to hide the emoji picker - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - expect(find.byType(EmojiHandler), findsNothing); - }); - - testWidgets('insert emoji by slash menu', (tester) async { - await prepare(tester); - await tester.editor.showSlashMenu(); - - /// show emoji picler - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_emoji.tr(), - offset: 100, - ); - await tester.pumpAndSettle(Duration(seconds: 1)); - expect(find.byType(EmojiHandler), findsOneWidget); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - - /// except the emoji is in document - expect(firstNode.delta!.toPlainText().contains('😀'), true); - }); - }); - - group('insert emoji by colon', () { - Future createNewDocumentAndShowEmojiList( - WidgetTester tester, { - String? search, - }) async { - await prepare(tester); - 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); + expect(find.byType(EmojiSelectionMenu), findsOneWidget); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart index 1d0f13eebc..4a38dde920 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart @@ -1,12 +1,13 @@ import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.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'; @@ -37,7 +38,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_light.tr(), ), ); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); + await tester.pumpAndSettle(); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.light); @@ -47,7 +48,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_dark.tr(), ), ); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); + await tester.pumpAndSettle(); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.dark); @@ -65,11 +66,10 @@ void main() { ], tester: tester, ); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); - // disable it temporarily. It works on macOS but not on Linux. - // themeMode = tester.widget(appFinder).themeMode; - // expect(themeMode, ThemeMode.light); + themeMode = tester.widget(appFinder).themeMode; + expect(themeMode, ThemeMode.light); }); testWidgets('show or hide home menu', (tester) async { 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 836cfe4ccd..f7d94e8b4a 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,6 +13,7 @@ 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 d7a505d152..4a117a71ff 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -67,10 +67,12 @@ extension CommonOperations on WidgetTester { } else { // cloud version final anonymousButton = find.byType(SignInAnonymousButtonV2); - await tapButton(anonymousButton, warnIfMissed: true); + await tapButton(anonymousButton); } - await pumpAndSettle(const Duration(milliseconds: 200)); + if (Platform.isWindows) { + 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 970965f294..5f27305fe5 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1,9 +1,17 @@ import 'dart:io'; +import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; +import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; @@ -19,11 +27,10 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; @@ -37,7 +44,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; @@ -70,8 +76,6 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; @@ -86,9 +90,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; // Non-exported member of the table_calendar library @@ -942,31 +943,6 @@ 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); @@ -1596,7 +1572,7 @@ extension AppFlowyDatabaseTest on WidgetTester { of: textField, matching: find.byWidgetPredicate( (widget) => - widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s, + widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m, ), ), ); 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 398a3f9657..491ac9432c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart @@ -307,11 +307,9 @@ class EditorOperations { Future openTurnIntoMenu(Path path) async { await hoverAndClickOptionMenuButton(path); await tester.tapButton( - find - .findTextInFlowyText( - LocaleKeys.document_plugins_optionAction_turnInto.tr(), - ) - .first, + find.findTextInFlowyText( + LocaleKeys.document_plugins_optionAction_turnInto.tr(), + ), ); 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 bfc5efedde..aade7bb4c9 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.toolbar_link_edit_m), + matching: find.byFlowySvg(FlowySvgs.edit_s), ); await tap(editUsernameFinder, warnIfMissed: false); await pumpAndSettle(); diff --git a/frontend/appflowy_flutter/lib/ai/ai.dart b/frontend/appflowy_flutter/lib/ai/ai.dart index 9bfeeb4e00..e3f52a8168 100644 --- a/frontend/appflowy_flutter/lib/ai/ai.dart +++ b/frontend/appflowy_flutter/lib/ai/ai.dart @@ -2,8 +2,6 @@ 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'; @@ -15,5 +13,4 @@ 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 b08fadb7f8..b8592bc32b 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart @@ -4,28 +4,6 @@ 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 deleted file mode 100644 index 0bcc41da9b..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ /dev/null @@ -1,181 +0,0 @@ -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 95854ab047..23d8879275 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart @@ -1,8 +1,12 @@ import 'dart:async'; -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:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -12,20 +16,19 @@ part 'ai_prompt_input_bloc.freezed.dart'; class AIPromptInputBloc extends Bloc { AIPromptInputBloc({ - required String objectId, required PredefinedFormat? predefinedFormat, - }) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), + }) : _listener = LocalLLMListener(), super(AIPromptInputState.initial(predefinedFormat)) { _dispatch(); _startListening(); _init(); } - final AIModelStateNotifier aiModelStateNotifier; + final LocalLLMListener _listener; @override Future close() async { - await aiModelStateNotifier.dispose(); + await _listener.stop(); return super.close(); } @@ -33,19 +36,42 @@ class AIPromptInputBloc extends Bloc { on( (event, emit) { event.when( - updateAIState: (aiType, editable, hintText) { + updateChatState: (LocalAIChatPB chatState) { + // Only user enable chat with file and the plugin is already running + final supportChatWithFile = chatState.fileEnabled && + chatState.pluginState.state == RunningStatePB.Running; + + final aiType = chatState.pluginState.state == RunningStatePB.Running + ? AIType.localAI + : AIType.appflowyAI; + emit( state.copyWith( aiType: aiType, - editable: editable, - hintText: hintText, + supportChatWithFile: supportChatWithFile, + chatState: chatState, + ), + ); + }, + updatePluginState: (LocalAIPluginStatePB chatState) { + final fileEnabled = state.chatState?.fileEnabled ?? false; + final supportChatWithFile = + fileEnabled && chatState.state == RunningStatePB.Running; + + final aiType = chatState.state == RunningStatePB.Running + ? AIType.localAI + : AIType.appflowyAI; + + emit( + state.copyWith( + supportChatWithFile: supportChatWithFile, + aiType: aiType, ), ); }, toggleShowPredefinedFormat: () { - final showPredefinedFormats = !state.showPredefinedFormats; final predefinedFormat = - showPredefinedFormats && state.predefinedFormat == null + !state.showPredefinedFormats && state.predefinedFormat == null ? PredefinedFormat( imageFormat: ImageFormat.text, textFormat: TextFormat.paragraph, @@ -53,15 +79,12 @@ class AIPromptInputBloc extends Bloc { : null; emit( state.copyWith( - showPredefinedFormats: showPredefinedFormats, + showPredefinedFormats: !state.showPredefinedFormats, predefinedFormat: predefinedFormat, ), ); }, updatePredefinedFormat: (format) { - if (!state.showPredefinedFormats) { - return; - } emit(state.copyWith(predefinedFormat: format)); }, attachFile: (filePath, fileName) { @@ -104,16 +127,29 @@ class AIPromptInputBloc extends Bloc { } void _startListening() { - aiModelStateNotifier.addListener( - onStateChanged: (aiType, editable, hintText) { - add(AIPromptInputEvent.updateAIState(aiType, editable, hintText)); + _listener.start( + stateCallback: (pluginState) { + if (!isClosed) { + add(AIPromptInputEvent.updatePluginState(pluginState)); + } + }, + chatStateCallback: (chatState) { + if (!isClosed) { + add(AIPromptInputEvent.updateChatState(chatState)); + } }, ); } void _init() { - final (aiType, hintText, isEditable) = aiModelStateNotifier.getState(); - add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText)); + AIEventGetLocalAIChatState().send().fold( + (chatState) { + if (!isClosed) { + add(AIPromptInputEvent.updateChatState(chatState)); + } + }, + Log.error, + ); } Map consumeMetadata() { @@ -132,12 +168,12 @@ class AIPromptInputBloc extends Bloc { @freezed class AIPromptInputEvent with _$AIPromptInputEvent { - const factory AIPromptInputEvent.updateAIState( - AiType aiType, - bool editable, - String hintText, - ) = _UpdateAIState; - + const factory AIPromptInputEvent.updateChatState( + LocalAIChatPB chatState, + ) = _UpdateChatState; + const factory AIPromptInputEvent.updatePluginState( + LocalAIPluginStatePB chatState, + ) = _UpdatePluginState; const factory AIPromptInputEvent.toggleShowPredefinedFormat() = _ToggleShowPredefinedFormat; const factory AIPromptInputEvent.updatePredefinedFormat( @@ -156,25 +192,30 @@ class AIPromptInputEvent with _$AIPromptInputEvent { @freezed class AIPromptInputState with _$AIPromptInputState { const factory AIPromptInputState({ - required AiType aiType, + required AIType aiType, required bool supportChatWithFile, required bool showPredefinedFormats, required PredefinedFormat? predefinedFormat, + required LocalAIChatPB? chatState, required List attachedFiles, required List mentionedPages, - required bool editable, - required String hintText, }) = _AIPromptInputState; factory AIPromptInputState.initial(PredefinedFormat? format) => AIPromptInputState( - aiType: AiType.cloud, + aiType: AIType.appflowyAI, supportChatWithFile: false, showPredefinedFormats: format != null, predefinedFormat: format, + chatState: null, attachedFiles: [], mentionedPages: [], - editable: true, - hintText: '', ); } + +enum AIType { + appflowyAI, + localAI; + + bool get isLocalAI => this == localAI; +} 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 39487652f8..2709b1d59c 100644 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart @@ -3,7 +3,6 @@ import 'dart:ffi'; import 'dart:isolate'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -15,26 +14,16 @@ 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, required String text, PredefinedFormat? format, - List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }); } @@ -45,27 +34,19 @@ class AppFlowyAIService implements AIRepository { required String text, PredefinedFormat? format, List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }) async { final stream = AppFlowyCompletionStream( onStart: onStart, - processMessage: processMessage, - processAssistMessage: processAssistMessage, - processError: onError, - onLocalAIStreamingStateChange: onLocalAIStreamingStateChange, + onProcess: onProcess, onEnd: onEnd, + onError: onError, ); - final records = history.map((record) => record.toPB()).toList(); - final payload = CompleteTextPB( text: text, completionType: completionType, @@ -76,7 +57,6 @@ class AppFlowyAIService implements AIRepository { if (objectId != null) objectId, ...sourceIds, ].unique(), - history: records, ); return AIEventCompleteText(payload).send().fold( @@ -92,30 +72,23 @@ class AppFlowyAIService implements AIRepository { abstract class CompletionStream { CompletionStream({ required this.onStart, - required this.processMessage, - required this.processAssistMessage, - required this.processError, - required this.onLocalAIStreamingStateChange, + required this.onProcess, required this.onEnd, + required this.onError, }); final Future Function() onStart; - 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(String text) onProcess; final Future Function() onEnd; + final void Function(AIError error) onError; } class AppFlowyCompletionStream extends CompletionStream { AppFlowyCompletionStream({ required super.onStart, - required super.processMessage, - required super.processAssistMessage, - required super.processError, + required super.onProcess, required super.onEnd, - required super.onLocalAIStreamingStateChange, + required super.onError, }) { _startListening(); } @@ -129,7 +102,51 @@ class AppFlowyCompletionStream extends CompletionStream { _port.handler = _controller.add; _subscription = _controller.stream.listen( (event) async { - await _handleEvent(event); + 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), + ); + } }, ); } @@ -139,66 +156,4 @@ 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 deleted file mode 100644 index 7ad52b9ec4..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -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/loading_indicator.dart b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart index 3a9c96b255..e21ea95ac1 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart @@ -16,50 +16,48 @@ class AILoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return SelectionContainer.disabled( - child: SizedBox( - height: 20, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: FlowyText( - text, - color: Theme.of(context).hintColor, - ), + return SizedBox( + height: 20, + child: SeparatedRow( + separatorBuilder: () => const HSpace(4), + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: FlowyText( + text, + color: Theme.of(context).hintColor, ), - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], - ), + ), + buildDot(const Color(0xFF9327FF)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(duration: slice * 2, begin: 0, end: 0), + buildDot(const Color(0xFFFB006D)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice, begin: 0, end: 0) + .then() + .slideY(begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0) + .then() + .slideY(begin: 0, end: 0), + buildDot(const Color(0xFFFFCE00)) + .animate(onPlay: (controller) => controller.repeat()) + .slideY(duration: slice * 2, begin: 0, end: 0) + .then() + .slideY(duration: slice, begin: 0, end: -1) + .then() + .slideY(begin: -1, end: 1) + .then() + .slideY(begin: 1, end: 0), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart index a2676f2c15..e2eeb80434 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,26 +17,20 @@ 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(); @@ -48,6 +42,7 @@ class _DesktopPromptInputState extends State { final overlayController = OverlayPortalController(); final inputControlCubit = ChatInputControlCubit(); final focusNode = FocusNode(); + final textController = TextEditingController(); late SendButtonState sendButtonState; bool isComposing = false; @@ -56,19 +51,18 @@ class _DesktopPromptInputState extends State { void initState() { super.initState(); - widget.textController.addListener(handleTextControllerChanged); - focusNode - ..addListener( - () { - if (!widget.hideDecoration) { - setState(() {}); // refresh border color - } - if (!focusNode.hasFocus) { - cancelMentionPage(); // hide menu when lost focus - } - }, - ) - ..onKeyEvent = handleKeyEvent; + textController.addListener(handleTextControllerChanged); + + focusNode.addListener( + () { + if (!widget.hideDecoration) { + setState(() {}); // refresh border color + } + if (!focusNode.hasFocus) { + cancelMentionPage(); // hide menu when lost focus + } + }, + ); updateSendButtonState(); @@ -86,7 +80,7 @@ class _DesktopPromptInputState extends State { @override void dispose() { focusNode.dispose(); - widget.textController.removeListener(handleTextControllerChanged); + textController.dispose(); inputControlCubit.close(); super.dispose(); } @@ -111,7 +105,7 @@ class _DesktopPromptInputState extends State { overlayChildBuilder: (context) { return PromptInputMentionPageMenu( anchor: PromptInputAnchor(textFieldKey, layerLink), - textController: widget.textController, + textController: textController, onPageSelected: handlePageSelected, ); }, @@ -141,11 +135,11 @@ class _DesktopPromptInputState extends State { children: [ ConstrainedBox( constraints: getTextFieldConstraints( - state.showPredefinedFormats && !widget.hideFormats, + state.showPredefinedFormats, ), child: inputTextField(), ), - if (state.showPredefinedFormats && !widget.hideFormats) + if (state.showPredefinedFormats) Positioned.fill( bottom: null, child: TextFieldTapRegion( @@ -154,13 +148,14 @@ class _DesktopPromptInputState extends State { start: 8.0, ), child: ChangeFormatBar( - showImageFormats: state.aiType.isCloud, predefinedFormat: state.predefinedFormat, spacing: 4.0, onSelectPredefinedFormat: (format) => context.read().add( AIPromptInputEvent - .updatePredefinedFormat(format), + .updatePredefinedFormat( + format, + ), ), ), ), @@ -170,9 +165,8 @@ class _DesktopPromptInputState extends State { top: null, child: TextFieldTapRegion( child: _PromptBottomActions( - showPredefinedFormatBar: + showPredefinedFormats: state.showPredefinedFormats, - showPredefinedFormatButton: !widget.hideFormats, onTogglePredefinedFormatSection: () => context.read().add( AIPromptInputEvent @@ -186,8 +180,6 @@ class _DesktopPromptInputState extends State { widget.selectedSourcesNotifier, onUpdateSelectedSources: widget.onUpdateSelectedSources, - extraBottomActionButton: - widget.extraBottomActionButton, ), ), ), @@ -226,12 +218,12 @@ class _DesktopPromptInputState extends State { if (!focusNode.hasFocus) { focusNode.requestFocus(); } - widget.textController.text += '@'; + textController.text += '@'; WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { context .read() - .startSearching(widget.textController.value); + .startSearching(textController.value); overlayController.show(); } }); @@ -247,7 +239,7 @@ class _DesktopPromptInputState extends State { void updateSendButtonState() { if (widget.isStreaming) { sendButtonState = SendButtonState.streaming; - } else if (widget.textController.text.trim().isEmpty) { + } else if (textController.text.trim().isEmpty) { sendButtonState = SendButtonState.disabled; } else { sendButtonState = SendButtonState.enabled; @@ -259,9 +251,9 @@ class _DesktopPromptInputState extends State { return; } final trimmedText = inputControlCubit.formatIntputText( - widget.textController.text.trim(), + textController.text.trim(), ); - widget.textController.clear(); + textController.clear(); if (trimmedText.isEmpty) { return; } @@ -284,17 +276,17 @@ class _DesktopPromptInputState extends State { setState(() { // update whether send button is clickable updateSendButtonState(); - isComposing = !widget.textController.value.composing.isCollapsed; + isComposing = !textController.value.composing.isCollapsed; }); if (isComposing) { return; } + // handle text and selection changes ONLY when mentioning a page + // disable mention return; - - // handle text and selection changes ONLY when mentioning a page // ignore: dead_code if (!overlayController.isShowing || inputControlCubit.filterStartPosition == -1) { @@ -302,7 +294,6 @@ 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 = @@ -349,27 +340,22 @@ class _DesktopPromptInputState extends State { } KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - // if (event.character == '@') { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // inputControlCubit.startSearching(widget.textController.value); - // overlayController.show(); - // }); - // } - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - node.unfocus(); - return KeyEventResult.handled; + if (event.character == '@') { + WidgetsBinding.instance.addPostFrameCallback((_) { + inputControlCubit.startSearching(textController.value); + overlayController.show(); + }); } return KeyEventResult.ignored; } void handlePageSelected(ViewPB view) { - final newText = widget.textController.text.replaceRange( + final newText = textController.text.replaceRange( inputControlCubit.filterStartPosition, inputControlCubit.filterEndPosition, view.id, ); - widget.textController.value = TextEditingValue( + textController.value = TextEditingValue( text: newText, selection: TextSelection.collapsed( offset: inputControlCubit.filterStartPosition + view.id.length, @@ -390,27 +376,18 @@ class _DesktopPromptInputState extends State { link: layerLink, child: BlocBuilder( builder: (context, state) { - Widget textField = PromptInputTextField( + return PromptInputTextField( key: textFieldKey, - editable: state.editable, cubit: inputControlCubit, - textController: widget.textController, + textController: textController, textFieldFocusNode: focusNode, contentPadding: calculateContentPadding(state.showPredefinedFormats), - hintText: state.hintText, + hintText: switch (state.aiType) { + AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), + AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() + }, ); - - if (!state.editable) { - textField = FlowyTooltip( - message: LocaleKeys - .settings_aiPage_keys_localAINotReadyTextFieldPrompt - .tr(), - child: textField, - ); - } - - return textField; }, ), ), @@ -515,7 +492,6 @@ class _FocusNextItemIntent extends Intent { class PromptInputTextField extends StatelessWidget { const PromptInputTextField({ super.key, - required this.editable, required this.cubit, required this.textController, required this.textFieldFocusNode, @@ -527,7 +503,6 @@ class PromptInputTextField extends StatelessWidget { final TextEditingController textController; final FocusNode textFieldFocusNode; final EdgeInsetsGeometry contentPadding; - final bool editable; final String hintText; @override @@ -535,8 +510,6 @@ class PromptInputTextField extends StatelessWidget { return ExtendedTextField( controller: textController, focusNode: textFieldFocusNode, - readOnly: !editable, - enabled: editable, decoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, @@ -574,19 +547,16 @@ class PromptInputTextField extends StatelessWidget { class _PromptBottomActions extends StatelessWidget { const _PromptBottomActions({ required this.sendButtonState, - required this.showPredefinedFormatBar, - required this.showPredefinedFormatButton, + required this.showPredefinedFormats, required this.onTogglePredefinedFormatSection, required this.onStartMention, required this.onSendPressed, required this.onStopStreaming, required this.selectedSourcesNotifier, required this.onUpdateSelectedSources, - this.extraBottomActionButton, }); - final bool showPredefinedFormatBar; - final bool showPredefinedFormatButton; + final bool showPredefinedFormats; final void Function() onTogglePredefinedFormatSection; final void Function() onStartMention; final SendButtonState sendButtonState; @@ -594,7 +564,6 @@ class _PromptBottomActions extends StatelessWidget { final void Function() onStopStreaming; final ValueNotifier> selectedSourcesNotifier; final void Function(List) onUpdateSelectedSources; - final Widget? extraBottomActionButton; @override Widget build(BuildContext context) { @@ -603,27 +572,18 @@ class _PromptBottomActions extends StatelessWidget { margin: DesktopAIChatSizes.inputActionBarMargin, child: BlocBuilder( builder: (context, state) { + if (state.chatState == null) { + return Align( + alignment: AlignmentDirectional.centerEnd, + child: _sendButton(), + ); + } return Row( children: [ - if (showPredefinedFormatButton) ...[ - _predefinedFormatButton(), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - SelectModelMenu( - aiModelStateNotifier: - context.read().aiModelStateNotifier, - ), + _predefinedFormatButton(), const Spacer(), - if (state.aiType.isCloud) ...[ - _selectSourcesButton(), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - if (extraBottomActionButton != null) ...[ - extraBottomActionButton!, + if (state.aiType == AIType.appflowyAI) ...[ + _selectSourcesButton(context), const HSpace( DesktopAIChatSizes.inputActionBarButtonSpacing, ), @@ -648,12 +608,12 @@ class _PromptBottomActions extends StatelessWidget { Widget _predefinedFormatButton() { return PromptInputDesktopToggleFormatButton( - showFormatBar: showPredefinedFormatBar, + showFormatBar: showPredefinedFormats, onTap: onTogglePredefinedFormatSection, ); } - Widget _selectSourcesButton() { + Widget _selectSourcesButton(BuildContext context) { 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 403b978905..189882ae4d 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart @@ -48,30 +48,25 @@ class ChangeFormatBar extends StatelessWidget { required this.predefinedFormat, required this.spacing, required this.onSelectPredefinedFormat, - this.showImageFormats = true, }); final PredefinedFormat? predefinedFormat; final double spacing; final void Function(PredefinedFormat) onSelectPredefinedFormat; - final bool showImageFormats; @override Widget build(BuildContext context) { - final showTextFormats = predefinedFormat?.imageFormat.hasText ?? true; return SizedBox( height: DesktopAIPromptSizes.predefinedFormatButtonHeight, child: SeparatedRow( mainAxisSize: MainAxisSize.min, separatorBuilder: () => HSpace(spacing), children: [ - if (showImageFormats) ...[ - _buildFormatButton(context, ImageFormat.text), - _buildFormatButton(context, ImageFormat.textAndImage), - _buildFormatButton(context, ImageFormat.image), - ], - if (showImageFormats && showTextFormats) _buildDivider(), - if (showTextFormats) ...[ + _buildFormatButton(context, ImageFormat.text), + _buildFormatButton(context, ImageFormat.textAndImage), + _buildFormatButton(context, ImageFormat.image), + if (predefinedFormat?.imageFormat.hasText ?? true) ...[ + _buildDivider(), _buildTextFormatButton(context, TextFormat.paragraph), _buildTextFormatButton(context, TextFormat.bulletList), _buildTextFormatButton(context, TextFormat.numberedList), @@ -104,7 +99,6 @@ class ChangeFormatBar extends StatelessWidget { }, child: FlowyTooltip( message: format.i18n, - preferBelow: false, child: SizedBox.square( dimension: _buttonSize, child: FlowyHover( @@ -151,7 +145,6 @@ 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 deleted file mode 100644 index a611d84310..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ /dev/null @@ -1,264 +0,0 @@ -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 51357e6a0b..d7c920c49c 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart @@ -145,7 +145,7 @@ class _IndicatorButton extends StatelessWidget { children: [ FlowySvg( FlowySvgs.ai_page_s, - color: Theme.of(context).hintColor, + color: Theme.of(context).iconTheme.color, ), 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(8), + size: const Size.square(10), ), ], ), diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart index cca6e65f63..20def4ca5e 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart @@ -1,6 +1,4 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -25,23 +23,6 @@ class PromptInputSendButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyIconButton( width: _buttonSize, - richTooltipText: switch (state) { - SendButtonState.streaming => TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.chat_stopTooltip.tr()} ', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: 'ESC', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ), - _ => null, - }, icon: switch (state) { SendButtonState.enabled => FlowySvg( FlowySvgs.ai_send_filled_s, diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index fd8aa03dfe..a27ab07e9d 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -44,27 +44,17 @@ Future afLaunchUri( uri = Uri.parse('https://$url'); } - /// opening an incorrect link will cause a system error dialog to pop up on macOS - /// only use [canLaunchUrl] on macOS - /// and there is an known issue with url_launcher on Linux where it fails to launch - /// see https://github.com/flutter/flutter/issues/88463 - bool result = true; - if (UniversalPlatform.isMacOS) { - result = await launcher.canLaunchUrl(uri); - } - - if (result) { - try { - // try to launch the uri directly - result = await launcher.launchUrl( - uri, - mode: mode, - webOnlyWindowName: webOnlyWindowName, - ); - } on PlatformException catch (e) { - Log.error('Failed to open uri: $e'); - return false; - } + // 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; } // if the uri is not a valid url, try to launch it with http scheme @@ -143,6 +133,7 @@ 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/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 15f3ada42e..986fab128b 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -100,10 +100,6 @@ bool get isAuthEnabled { return false; } -bool get isLocalAuthEnabled { - return currentCloudType().isLocal; -} - /// Determines if AppFlowy Cloud is enabled. bool get isAppFlowyCloudEnabled { return currentCloudType().isAppFlowyCloudEnabled; diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index 157be012b1..56a61e120b 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -16,8 +16,6 @@ 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 = { @@ -88,7 +86,6 @@ class AFDropdownMenu extends StatefulWidget { this.requestFocusOnTap, this.expandedInsets, this.searchCallback, - this.selectOptionCompare, required this.dropdownMenuEntries, }); @@ -270,11 +267,6 @@ 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(); } @@ -309,16 +301,7 @@ class _AFDropdownMenuState extends State> { filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); final int index = filteredEntries.indexWhere( - (DropdownMenuEntry entry) { - if (widget.selectOptionCompare != null) { - return widget.selectOptionCompare!( - entry.value, - widget.initialSelection, - ); - } else { - return entry.value == widget.initialSelection; - } - }, + (DropdownMenuEntry entry) => 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 0527316860..1480cc02e9 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,13 +19,14 @@ class UserProfileBloc extends Bloc { Future _initialize(Emitter emit) async { emit(const UserProfileState.loading()); - final latestOrFailure = + + final workspaceOrFailure = await FolderEventGetCurrentWorkspaceSetting().send(); final userOrFailure = await getIt().getUser(); - final latest = latestOrFailure.fold( - (latestPB) => latestPB, + final workspaceSetting = workspaceOrFailure.fold( + (workspaceSettingPB) => workspaceSettingPB, (error) => null, ); @@ -34,13 +35,13 @@ class UserProfileBloc extends Bloc { (error) => null, ); - if (latest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return emit(const UserProfileState.workspaceFailure()); } emit( UserProfileState.success( - workspaceSettings: latest, + workspaceSettings: workspaceSetting, userProfile: userProfile, ), ); @@ -58,7 +59,7 @@ class UserProfileState with _$UserProfileState { const factory UserProfileState.loading() = _Loading; const factory UserProfileState.workspaceFailure() = _WorkspaceFailure; const factory UserProfileState.success({ - required WorkspaceLatestPB workspaceSettings, + required WorkspaceSettingPB 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 318b06394a..792679daf1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -336,6 +336,7 @@ class _MobileViewPageState extends State { listener: (context, state) { if (state.isLocked) { showToastNotification( + context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); @@ -365,6 +366,7 @@ 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 be134e0a92..dd659420d6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -66,7 +66,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { break; case MobileViewBottomSheetBodyAction.delete: context.read().add(const ViewEvent.delete()); - Navigator.of(context).pop(); + context.pop(); break; case MobileViewBottomSheetBodyAction.addToFavorites: _addFavorite(context); @@ -161,6 +161,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { context.pop(); showToastNotification( + context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); } @@ -169,6 +170,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( + context, message: LocaleKeys.button_favoriteSuccessfully.tr(), ); } @@ -177,6 +179,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { _toggleFavorite(context); showToastNotification( + context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); } @@ -199,7 +202,8 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copy.tr(), ); } } @@ -230,10 +234,12 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { ), ); showToastNotification( + context, message: LocaleKeys.shareAction_copyLinkSuccess.tr(), ); } else { showToastNotification( + context, message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(), type: ToastificationType.error, ); @@ -317,9 +323,11 @@ 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, ), @@ -327,9 +335,11 @@ 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, @@ -339,6 +349,7 @@ 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 86021ea938..c1129af79d 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,6 +65,7 @@ class _MobileViewItemBottomSheetState extends State { Navigator.pop(context); context.read().add(const ViewEvent.duplicate()); showToastNotification( + context, message: LocaleKeys.button_duplicateSuccessfully.tr(), ); break; @@ -83,6 +84,7 @@ class _MobileViewItemBottomSheetState extends State { .read() .add(FavoriteEvent.toggle(widget.view)); showToastNotification( + context, message: !widget.view.isFavorite ? LocaleKeys.button_favoriteSuccessfully.tr() : LocaleKeys.button_unfavoriteSuccessfully.tr(), @@ -144,6 +146,7 @@ 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 47ab37505e..991cf82b5d 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, @@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { final userProfile = context.read().state.userProfilePB; // the publish feature is only available for AppFlowy Cloud if (userProfile == null || - userProfile.workspaceAuthType != AuthTypePB.Server) { + userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { return []; } @@ -236,7 +236,6 @@ class MobileViewBottomSheetBody extends StatelessWidget { MobileViewBottomSheetBodyAction.unpublish, ), ), - _divider(), ]; } else { return [ @@ -247,7 +246,6 @@ 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 d4b4292443..cb840b0f40 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -45,6 +45,7 @@ enum MobilePaneActionType { size: 24.0, onPressed: (context) { showToastNotification( + context, message: LocaleKeys.button_unfavoriteSuccessfully.tr(), ); @@ -60,6 +61,7 @@ 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 b0f21188cd..fa3494002d 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(message: 'Failed to open row page'); + showToastNotification(context, 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 0e7a7cb4c6..e6d2d895b1 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 latest = snapshots.data?[0].fold( - (latest) { - return latest as WorkspaceLatestPB?; + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) { + return workspaceSettingPB as WorkspaceSettingPB?; }, (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 (latest == null || userProfile == null) { + if (workspaceSetting == 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 345a4591d1..2d409f58b6 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 workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) { - return workspaceLatestPB as WorkspaceLatestPB?; + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) { + return workspaceSettingPB as WorkspaceSettingPB?; }, (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 (workspaceLatest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -78,7 +78,7 @@ class MobileHomeScreen extends StatelessWidget { value: userProfile, child: MobileHomePage( userProfile: userProfile, - workspaceLatest: workspaceLatest, + workspaceSetting: workspaceSetting, ), ), ), @@ -95,11 +95,11 @@ class MobileHomePage extends StatefulWidget { const MobileHomePage({ super.key, required this.userProfile, - required this.workspaceLatest, + required this.workspaceSetting, }); final UserProfilePB userProfile; - final WorkspaceLatestPB workspaceLatest; + final WorkspaceSettingPB workspaceSetting; @override State createState() => _MobileHomePageState(); @@ -145,7 +145,7 @@ class _MobileHomePageState extends State { void _onLatestViewChange() async { final id = getIt().latestOpenView?.id; - if (id == null || id.isEmpty) { + if (id == null) { return; } await FolderEventSetLatestView(ViewIdPB(value: id)).send(); @@ -329,7 +329,7 @@ class _HomePageState extends State<_HomePage> { } if (message != null) { - showToastNotification(message: message, type: toastType); + showToastNotification(context, 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 113f12e543..97cc243c9e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -194,7 +194,6 @@ 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 a01df20549..b5845f763e 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,6 +71,12 @@ 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()), @@ -94,12 +100,13 @@ class _MobileHomeSettingPageState extends State { key: ValueKey(currentWorkspaceId), userProfile: userProfile, workspaceId: currentWorkspaceId, + currentWorkspaceMemberRole: state.currentWorkspace?.role, ), const SupportSettingGroup(), const AboutSettingGroup(), UserSessionSettingGroup( userProfile: userProfile, - showThirdPartyLogin: false, + showThirdPartyLogin: isLocalAuthEnabled, ), 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 659473a6b1..cbbda8362a 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,7 +16,6 @@ enum _MobileSettingsPopupMenuItem { members, trash, help, - helpAndDocumentation, } class HomePageSettingsPopupMenu extends StatelessWidget { @@ -48,7 +47,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget { text: LocaleKeys.settings_popupMenuItem_settings.tr(), ), // only show the member items in cloud mode - if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[ + if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ const PopupMenuDivider(height: 0.5), _buildItem( value: _MobileSettingsPopupMenuItem.members, @@ -63,16 +62,10 @@ 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_getSupport.tr(), + text: LocaleKeys.settings_popupMenuItem_helpAndSupport.tr(), ), ], onSelected: (_MobileSettingsPopupMenuItem value) { @@ -89,9 +82,6 @@ class HomePageSettingsPopupMenu extends StatelessWidget { case _MobileSettingsPopupMenuItem.help: _openHelpPage(context); break; - case _MobileSettingsPopupMenuItem.helpAndDocumentation: - _openHelpAndDocumentationPage(context); - break; } }, child: const Padding( @@ -133,10 +123,6 @@ 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 485e07a28c..0197f34940 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,6 +339,7 @@ class _SpaceMenuItemTrailingState extends State { context.read().add(const SpaceEvent.duplicate()); showToastNotification( + context, message: LocaleKeys.space_success_duplicateSpace.tr(), ); @@ -373,6 +374,7 @@ class _SpaceMenuItemTrailingState extends State { .add(SpaceEvent.rename(space: widget.space, name: name)); showToastNotification( + context, message: LocaleKeys.space_success_renameSpace.tr(), ); }, @@ -422,6 +424,7 @@ class _SpaceMenuItemTrailingState extends State { ); showToastNotification( + context, message: LocaleKeys.space_success_updateSpace.tr(), ); @@ -454,6 +457,7 @@ 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 56f5f3e6ab..7ebfeefbbc 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -167,7 +167,8 @@ class _MobileSpaceTabState extends State children: [ MobileHomeSpace(userProfile: widget.userProfile), // only show ai chat button for cloud user - if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) + if (widget.userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) 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 d306f48964..ef7f4492a5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -123,7 +123,6 @@ 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 f340319254..862c9876f2 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.iconBuilder != null; + final hasIcon = item.icon != null; return Container( height: 36, decoration: BoxDecoration( @@ -119,7 +119,7 @@ class MobileInlineActionsWidget extends StatelessWidget { child: Row( children: [ if (hasIcon) ...[ - item.iconBuilder!.call(isSelected), + item.icon!.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 3c6adb8627..170ef46ac2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -332,6 +332,7 @@ class _NotificationNavigationBar extends StatelessWidget { } showToastNotification( + context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -349,6 +350,7 @@ 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 33c2eb3905..a8055b8ba2 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: (workspaceLatest, userProfile) => + success: (workspaceSetting, userProfile) => _NotificationScreenContent( - workspaceLatest: workspaceLatest, + workspaceSetting: workspaceSetting, userProfile: userProfile, controller: controller, reminderBloc: reminderBloc, @@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State class _NotificationScreenContent extends StatelessWidget { const _NotificationScreenContent({ - required this.workspaceLatest, + required this.workspaceSetting, required this.userProfile, required this.controller, required this.reminderBloc, }); - final WorkspaceLatestPB workspaceLatest; + final WorkspaceSettingPB workspaceSetting; final UserProfilePB userProfile; final TabController controller; final ReminderBloc reminderBloc; @@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget { ..add( SidebarSectionsEvent.initial( userProfile, - workspaceLatest.workspaceId, + workspaceSetting.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 e694f9932d..dfa277f2ef 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,6 +108,7 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onMarkAllAsRead(BuildContext context) { showToastNotification( + context, message: LocaleKeys .settings_notifications_markAsReadNotifications_allSuccess .tr(), @@ -118,6 +119,7 @@ class NotificationSettingsPopupMenu extends StatelessWidget { void _onArchiveAll(BuildContext context) { showToastNotification( + context, message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess .tr(), ); @@ -131,6 +133,7 @@ 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 d1216eed98..85f468c76c 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,6 +31,7 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( + context, message: LocaleKeys .settings_notifications_markAsReadNotifications_success .tr(), @@ -54,6 +55,7 @@ enum NotificationPaneActionType { size: 24.0, onPressed: (context) { showToastNotification( + context, message: 'Unarchive notification success', ); @@ -166,6 +168,7 @@ class _NotificationMoreActions extends StatelessWidget { Navigator.of(context).pop(); showToastNotification( + context, message: LocaleKeys.settings_notifications_markAsReadNotifications_success .tr(), ); @@ -188,6 +191,7 @@ 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 45e801e07c..7dda8f0a14 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart @@ -74,6 +74,7 @@ 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 f69360575a..b65c9f9347 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,4 +1,5 @@ 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'; @@ -35,16 +36,12 @@ class MobileSelectionMenu extends SelectionMenuService { Alignment _alignment = Alignment.topLeft; final int itemCountFilter; final int startOffset; - ValueNotifier<_Position> _positionNotifier = ValueNotifier(_Position.zero); @override void dismiss() { if (_selectionMenuEntry != null) { editorState.service.keyboardService?.enable(); editorState.service.scrollService?.enable(); - editorState - .removeScrollViewScrolledListener(_checkPositionAfterScrolling); - _positionNotifier.dispose(); } _selectionMenuEntry?.remove(); @@ -56,21 +53,23 @@ class MobileSelectionMenu extends SelectionMenuService { final completer = Completer(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _show(); - editorState.addScrollViewScrolledListener(_checkPositionAfterScrolling); completer.complete(); }); return completer.future; } void _show() { - final position = _getCurrentPosition(); - if (position == null) return; + final selectionRects = editorState.selectionRects(); + if (selectionRects.isEmpty) { + return; + } + + calculateSelectionMenuOffset(selectionRects.first); + final (left, top, right, bottom) = getPosition(); final editorHeight = editorState.renderBox!.size.height; final editorWidth = editorState.renderBox!.size.width; - _positionNotifier = ValueNotifier(position); - final showAtTop = position.top != null; _selectionMenuEntry = OverlayEntry( builder: (context) { return SizedBox( @@ -81,55 +80,47 @@ class MobileSelectionMenu extends SelectionMenuService { onTap: dismiss, child: Stack( children: [ - ValueListenableBuilder( - valueListenable: _positionNotifier, - builder: (context, value, _) { - return Positioned( - top: value.top, - bottom: value.bottom, - left: value.left, - right: value.right, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: MobileSelectionMenuWidget( - selectionMenuStyle: style, - singleColumn: singleColumn, - showAtTop: showAtTop, - items: selectionMenuItems - ..forEach((element) { - if (element is MobileSelectionMenuItem) { - element.deleteSlash = false; - element.deleteKeywords = - deleteKeywordsByDefault; - for (final e in element.children) { - e.deleteSlash = deleteSlashByDefault; - e.deleteKeywords = deleteKeywordsByDefault; - e.onSelected = () { - dismiss(); - }; - } - } else { - element.deleteSlash = deleteSlashByDefault; - element.deleteKeywords = - deleteKeywordsByDefault; - element.onSelected = () { - dismiss(); - }; - } - }), - maxItemInRow: 5, - editorState: editorState, - itemCountFilter: itemCountFilter, - startOffset: startOffset, - menuService: this, - onExit: () { - dismiss(); - }, - deleteSlashByDefault: deleteSlashByDefault, - ), - ), - ); - }, + Positioned( + top: top, + bottom: bottom, + left: left, + right: right, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MobileSelectionMenuWidget( + selectionMenuStyle: style, + singleColumn: singleColumn, + items: selectionMenuItems + ..forEach((element) { + if (element is MobileSelectionMenuItem) { + element.deleteSlash = false; + element.deleteKeywords = deleteKeywordsByDefault; + for (final e in element.children) { + e.deleteSlash = deleteSlashByDefault; + e.deleteKeywords = deleteKeywordsByDefault; + e.onSelected = () { + dismiss(); + }; + } + } else { + element.deleteSlash = deleteSlashByDefault; + element.deleteKeywords = deleteKeywordsByDefault; + element.onSelected = () { + dismiss(); + }; + } + }), + maxItemInRow: 5, + editorState: editorState, + itemCountFilter: itemCountFilter, + startOffset: startOffset, + menuService: this, + onExit: () { + dismiss(); + }, + deleteSlashByDefault: deleteSlashByDefault, + ), + ), ), ], ), @@ -144,35 +135,6 @@ class MobileSelectionMenu extends SelectionMenuService { editorState.service.scrollService?.disable(); } - /// the workaround for: editor auto scrolling that will cause wrong position - /// of slash menu - void _checkPositionAfterScrolling() { - final position = _getCurrentPosition(); - if (position == null) return; - if (position == _positionNotifier.value) { - Future.delayed(const Duration(milliseconds: 100)).then((_) { - final position = _getCurrentPosition(); - if (position == null) return; - if (position != _positionNotifier.value) { - _positionNotifier.value = position; - } - }); - } else { - _positionNotifier.value = position; - } - } - - _Position? _getCurrentPosition() { - final selectionRects = editorState.selectionRects(); - if (selectionRects.isEmpty) { - return null; - } - final screenSize = MediaQuery.of(context).size; - calculateSelectionMenuOffset(selectionRects.first, screenSize); - final (left, top, right, bottom) = getPosition(); - return _Position(left, top, right, bottom); - } - @override Alignment get alignment { return _alignment; @@ -204,93 +166,55 @@ class MobileSelectionMenu extends SelectionMenuService { bottom = offset.dy; break; } + return (left, top, right, bottom); } - void calculateSelectionMenuOffset(Rect rect, Size screenSize) { + void calculateSelectionMenuOffset(Rect rect) { // Workaround: We can customize the padding through the [EditorStyle], // but the coordinates of overlay are not properly converted currently. // Just subtract the padding here as a result. - const menuHeight = 192.0, menuWidth = 240.0; + const menuHeight = 192.0, menuWidth = 240.0 + 10; + const menuOffset = Offset(0, 10); 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.bottomRight; - final bottomRight = rect.topLeft; - final offset = bottomRight; - final limitX = editorWidth + editorOffset.dx - menuWidth, - limitY = screenHeight - - editorHeight + - editorOffset.dy - - menuHeight - - rectHeight; + _alignment = Alignment.topLeft; + final bottomRight = rect.bottomRight; + final topRight = rect.topRight; + var offset = bottomRight + menuOffset; _offset = Offset( - editorWidth - offset.dx - menuWidth, - screenHeight - offset.dy - menuHeight - rectHeight, + offset.dx, + offset.dy, ); + // show above if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { - /// show above - if (offset.dy > menuHeight) { - _offset = Offset( - _offset.dx, - offset.dy - menuHeight, - ); - _alignment = Alignment.topRight; - } else { - _offset = Offset( - _offset.dx, - limitY, - ); - } + offset = topRight - menuOffset; + _alignment = Alignment.bottomLeft; + + final limitX = editorWidth - menuWidth; + _offset = Offset( + min(offset.dx, limitX), + MediaQuery.of(context).size.height - 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, - ); - } + // 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; + final limitX = editorWidth - menuWidth + editorOffset.dx; + _offset = Offset( + min(x, limitX), + _offset.dy, + ); } } } - -class _Position { - const _Position(this.left, this.top, this.right, this.bottom); - - final double? left; - final double? top; - final double? right; - final double? bottom; - - static const _Position zero = _Position(0, 0, 0, 0); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _Position && - runtimeType == other.runtimeType && - left == other.left && - top == other.top && - right == other.right && - bottom == other.bottom; - - @override - int get hashCode => - left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart index bdee8f1857..ae5b0b11ac 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart @@ -36,7 +36,9 @@ class MobileSelectionMenuItemWidget extends StatelessWidget { ), style: ButtonStyle( alignment: Alignment.centerLeft, - overlayColor: WidgetStateProperty.all(Colors.transparent), + overlayColor: WidgetStateProperty.all( + style.selectionMenuItemSelectedColor, + ), backgroundColor: isSelected ? WidgetStateProperty.all( style.selectionMenuItemSelectedColor, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart index d96dd224e1..ed080e6186 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart @@ -1,8 +1,6 @@ import 'dart:math'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'mobile_selection_menu_item.dart'; @@ -22,7 +20,6 @@ class MobileSelectionMenuWidget extends StatefulWidget { required this.deleteSlashByDefault, required this.singleColumn, required this.startOffset, - required this.showAtTop, this.nameBuilder, }); @@ -39,7 +36,6 @@ class MobileSelectionMenuWidget extends StatefulWidget { final bool deleteSlashByDefault; final bool singleColumn; - final bool showAtTop; final int startOffset; final SelectionMenuItemNameBuilder? nameBuilder; @@ -174,37 +170,27 @@ class _MobileSelectionMenuWidgetState extends State { @override Widget build(BuildContext context) { - 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, - ), + 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), ), - ), - if (!widget.showAtTop) Spacer(), - ], + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: _showingItems.isEmpty + ? _buildNoResultsWidget(context) + : _buildResultsWidget( + context, + _showingItems, + widget.itemCountFilter, + ), ), ); } @@ -328,32 +314,15 @@ class _MobileSelectionMenuWidgetState extends State { } Widget _buildNoResultsWidget(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(12.0), - ), + return const Padding( + padding: EdgeInsets.all(8.0), child: SizedBox( - width: 240, - height: 48, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Material( - color: Colors.transparent, - child: Center( - child: Text( - LocaleKeys.inlineActions_noResults.tr(), - style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)), - textAlign: TextAlign.center, - ), - ), + width: 140, + child: Material( + child: Text( + "No results", + style: TextStyle(fontSize: 18.0, color: Colors.grey), + textAlign: TextAlign.center, ), ), ), 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 2d5a3176cd..d4f0766626 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.com/privacy'), + onTap: () => afLaunchUrlString('https://appflowy.io/privacy'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_termsAndConditions.tr(), trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.com/terms'), + onTap: () => afLaunchUrlString('https://appflowy.io/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 b43ada6e42..f67cc9e6b8 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,6 +5,8 @@ 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'; @@ -16,10 +18,12 @@ 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) { @@ -28,6 +32,7 @@ class AiSettingsGroup extends StatelessWidget { create: (context) => SettingsAIBloc( userProfile, workspaceId, + currentWorkspaceMemberRole, )..add(const SettingsAIEvent.started()), child: BlocBuilder( builder: (context, state) { @@ -43,7 +48,7 @@ class AiSettingsGroup extends StatelessWidget { children: [ Flexible( child: FlowyText( - state.availableModels?.selectedModel.name ?? "", + state.selectedAIModel, color: theme.colorScheme.onSurface, overflow: TextOverflow.ellipsis, ), @@ -79,19 +84,16 @@ class AiSettingsGroup extends StatelessWidget { title: LocaleKeys.settings_aiPage_keys_llmModelType.tr(), builder: (_) { return Column( - children: (availableModels?.models ?? []) - .asMap() - .entries - .map( - (entry) => FlowyOptionTile.checkbox( - text: entry.value.name, - showTopBorder: entry.key == 0, - isSelected: - availableModels?.selectedModel.name == entry.value.name, + children: availableModels + .mapIndexed( + (index, model) => FlowyOptionTile.checkbox( + text: model, + showTopBorder: index == 0, + isSelected: state.selectedAIModel == model, onTap: () { context .read() - .add(SettingsAIEvent.selectModel(entry.value)); + .add(SettingsAIEvent.selectModel(model)); context.pop(); }, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart index 02d620e559..24c50f7ae6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -19,7 +18,6 @@ class AppFlowyCloudPage extends StatelessWidget { ), body: SettingCloud( restartAppFlowy: () async { - await getIt().signOut(); await runAppFlowy(); }, ), 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 28ebdb750e..cfdf3defb0 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,3 +1,5 @@ +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'; @@ -5,10 +7,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 { @@ -30,7 +32,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { selector: (state) => state.userProfile.name, builder: (context, userName) { return MobileSettingGroup( - groupTitle: LocaleKeys.settings_accountPage_title.tr(), + groupTitle: LocaleKeys.settings_mobile_personalInfo.tr(), settingItemList: [ MobileSettingItem( name: userName, @@ -58,7 +60,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { userName: userName, onSubmitted: (value) => context .read() - .add(SettingsUserEvent.updateUserName(name: value)), + .add(SettingsUserEvent.updateUserName(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 e5e4efef77..584b867736 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,6 +81,7 @@ 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 5ca5525099..b3b7cb71c5 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.workspaceAuthType == AuthTypePB.Server) ...[ + if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[ const VSpace(16.0), MobileLogoutButton( text: LocaleKeys.button_deleteAccount.tr(), @@ -63,15 +63,8 @@ class UserSessionSettingGroup extends StatelessWidget { ); }, builder: (context, state) { - return Column( - children: [ - const ContinueWithEmailAndPassword(), - const VSpace(12.0), - const ThirdPartySignInButtons( - expanded: true, - ), - const VSpace(16.0), - ], + return const ThirdPartySignInButtons( + expanded: true, ); }, ), 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 18bce0588b..2e805c5c5a 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 @@ -197,10 +197,11 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.addByEmail) { + if (actionType == WorkspaceMemberActionType.add) { result.fold( (s) { showToastNotification( + context, message: LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -217,16 +218,18 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( + context, type: ToastificationType.error, bottomPadding: keyboardHeight, message: message, ); }, ); - } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { + } else if (actionType == WorkspaceMemberActionType.invite) { result.fold( (s) { showToastNotification( + context, message: LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), bottomPadding: keyboardHeight, @@ -244,16 +247,18 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; }); showToastNotification( + context, type: ToastificationType.error, message: message, bottomPadding: keyboardHeight, ); }, ); - } else if (actionType == WorkspaceMemberActionType.removeByEmail) { + } else if (actionType == WorkspaceMemberActionType.remove) { result.fold( (s) { showToastNotification( + context, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceSuccess .tr(), @@ -262,6 +267,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { }, (f) { showToastNotification( + context, type: ToastificationType.error, message: LocaleKeys .settings_appearance_members_removeFromWorkspaceFailed @@ -276,15 +282,15 @@ class _InviteMemberPageState extends State<_InviteMemberPage> { void _inviteMember(BuildContext context) { final email = emailController.text; if (!isEmail(email)) { - showToastNotification( + return showToastNotification( + context, type: ToastificationType.error, message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); - return; } context .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); + .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); // clear the email field after inviting emailController.clear(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart index b2805d5857..501fd18ef7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart @@ -178,7 +178,7 @@ class _MemberItem extends StatelessWidget { showBottomBorder: false, onTap: () { workspaceMemberBloc.add( - WorkspaceMemberEvent.removeWorkspaceMemberByEmail( + WorkspaceMemberEvent.removeWorkspaceMember( member.email, ), ); 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 deleted file mode 100644 index 2cfc349bf8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart +++ /dev/null @@ -1,53 +0,0 @@ -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_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index 47c1668a2c..60fca000c0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -23,126 +23,11 @@ class ChatAIMessageBloc extends Bloc { parseMetadata(refSourceJsonString), ), ) { - _registerEventHandlers(); - _initializeStreamListener(); - _checkInitialStreamState(); - } + _dispatch(); - final String chatId; - final Int64? questionId; - - void _registerEventHandlers() { - on<_UpdateText>((event, emit) { - emit( - state.copyWith( - text: event.text, - messageState: const MessageState.ready(), - ), - ); - }); - - on<_ReceiveError>((event, emit) { - emit(state.copyWith(messageState: MessageState.onError(event.error))); - }); - - on<_Retry>((event, emit) async { - if (questionId == null) { - Log.error("Question id is not valid: $questionId"); - return; - } - emit(state.copyWith(messageState: const MessageState.loading())); - final payload = ChatMessageIdPB( - chatId: chatId, - messageId: questionId, - ); - final result = await AIEventGetAnswerForQuestion(payload).send(); - if (!isClosed) { - result.fold( - (answer) => add(ChatAIMessageEvent.retryResult(answer.content)), - (err) { - Log.error("Failed to get answer: $err"); - add(ChatAIMessageEvent.receiveError(err.toString())); - }, - ); - } - }); - - on<_RetryResult>((event, emit) { - emit( - state.copyWith( - text: event.text, - messageState: const MessageState.ready(), - ), - ); - }); - - on<_OnAIResponseLimit>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onAIResponseLimit(), - ), - ); - }); - - on<_OnAIImageResponseLimit>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onAIImageResponseLimit(), - ), - ); - }); - - on<_OnAIMaxRquired>((event, emit) { - emit( - state.copyWith( - messageState: MessageState.onAIMaxRequired(event.message), - ), - ); - }); - - on<_OnLocalAIInitializing>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onInitializingLocalAI(), - ), - ); - }); - - on<_ReceiveMetadata>((event, emit) { - Log.debug("AI Steps: ${event.metadata.progress?.step}"); - emit( - state.copyWith( - sources: event.metadata.sources, - progress: event.metadata.progress, - ), - ); - }); - } - - void _initializeStreamListener() { if (state.stream != null) { - state.stream!.listen( - onData: (text) => _safeAdd(ChatAIMessageEvent.updateText(text)), - onError: (error) => - _safeAdd(ChatAIMessageEvent.receiveError(error.toString())), - onAIResponseLimit: () => - _safeAdd(const ChatAIMessageEvent.onAIResponseLimit()), - onAIImageResponseLimit: () => - _safeAdd(const ChatAIMessageEvent.onAIImageResponseLimit()), - onMetadata: (metadata) => - _safeAdd(ChatAIMessageEvent.receiveMetadata(metadata)), - onAIMaxRequired: (message) { - Log.info(message); - _safeAdd(ChatAIMessageEvent.onAIMaxRequired(message)); - }, - onLocalAIInitializing: () => - _safeAdd(const ChatAIMessageEvent.onLocalAIInitializing()), - ); - } - } + _startListening(); - void _checkInitialStreamState() { - if (state.stream != null) { if (state.stream!.aiLimitReached) { add(const ChatAIMessageEvent.onAIResponseLimit()); } else if (state.stream!.error != null) { @@ -151,10 +36,130 @@ class ChatAIMessageBloc extends Bloc { } } - void _safeAdd(ChatAIMessageEvent event) { - if (!isClosed) { - add(event); - } + final String chatId; + final Int64? questionId; + + void _dispatch() { + on( + (event, emit) { + event.when( + updateText: (newText) { + emit( + state.copyWith( + text: newText, + messageState: const MessageState.ready(), + ), + ); + }, + receiveError: (error) { + emit(state.copyWith(messageState: MessageState.onError(error))); + }, + retry: () { + if (questionId is! Int64) { + Log.error("Question id is not Int64: $questionId"); + return; + } + emit( + state.copyWith( + messageState: const MessageState.loading(), + ), + ); + + final payload = ChatMessageIdPB( + chatId: chatId, + messageId: questionId, + ); + AIEventGetAnswerForQuestion(payload).send().then((result) { + if (!isClosed) { + result.fold( + (answer) { + add(ChatAIMessageEvent.retryResult(answer.content)); + }, + (err) { + Log.error("Failed to get answer: $err"); + add(ChatAIMessageEvent.receiveError(err.toString())); + }, + ); + } + }); + }, + retryResult: (String text) { + emit( + state.copyWith( + text: text, + messageState: const MessageState.ready(), + ), + ); + }, + onAIResponseLimit: () { + emit( + state.copyWith( + messageState: const MessageState.onAIResponseLimit(), + ), + ); + }, + onAIImageResponseLimit: () { + emit( + state.copyWith( + messageState: const MessageState.onAIImageResponseLimit(), + ), + ); + }, + onAIMaxRequired: (message) { + emit( + state.copyWith( + messageState: MessageState.onAIMaxRequired(message), + ), + ); + }, + receiveMetadata: (metadata) { + Log.debug("AI Steps: ${metadata.progress?.step}"); + emit( + state.copyWith( + sources: metadata.sources, + progress: metadata.progress, + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + state.stream!.listen( + onData: (text) { + if (!isClosed) { + add(ChatAIMessageEvent.updateText(text)); + } + }, + onError: (error) { + if (!isClosed) { + add(ChatAIMessageEvent.receiveError(error.toString())); + } + }, + onAIResponseLimit: () { + if (!isClosed) { + add(const ChatAIMessageEvent.onAIResponseLimit()); + } + }, + onAIImageResponseLimit: () { + if (!isClosed) { + add(const ChatAIMessageEvent.onAIImageResponseLimit()); + } + }, + onMetadata: (metadata) { + if (!isClosed) { + add(ChatAIMessageEvent.receiveMetadata(metadata)); + } + }, + onAIMaxRequired: (message) { + if (!isClosed) { + Log.info(message); + add(ChatAIMessageEvent.onAIMaxRequired(message)); + } + }, + ); } } @@ -169,8 +174,6 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent { _OnAIImageResponseLimit; const factory ChatAIMessageEvent.onAIMaxRequired(String message) = _OnAIMaxRquired; - const factory ChatAIMessageEvent.onLocalAIInitializing() = - _OnLocalAIInitializing; const factory ChatAIMessageEvent.receiveMetadata( MetadataCollection metadata, ) = _ReceiveMetadata; @@ -206,7 +209,6 @@ class MessageState with _$MessageState { const factory MessageState.onAIResponseLimit() = _AIResponseLimit; const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit; const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired; - const factory MessageState.onInitializingLocalAI() = _LocalAIInitializing; const factory MessageState.ready() = _Ready; const factory MessageState.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 602b46f97a..e7aca346e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -239,9 +239,9 @@ class ChatBloc extends Bloc { ), ); }, - regenerateAnswer: (id, format, model) { + regenerateAnswer: (id, format) { _clearRelatedQuestions(); - _regenerateAnswer(id, format, model); + _regenerateAnswer(id, format); 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,7 +483,6 @@ class ChatBloc extends Bloc { void _regenerateAnswer( String answerMessageIdString, PredefinedFormat? format, - AIModelPB? model, ) async { final id = temporaryMessageIDMap.entries .firstWhereOrNull((e) => e.value == answerMessageIdString) @@ -506,9 +505,6 @@ class ChatBloc extends Bloc { if (format != null) { payload.format = format.toPB(); } - if (model != null) { - payload.model = model; - } await AIEventRegenerateResponse(payload).send().fold( (success) { @@ -641,7 +637,6 @@ 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_member_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart index 2547ff668e..8718255cd9 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart @@ -27,7 +27,6 @@ class ChatMemberBloc extends Bloc { final payload = WorkspaceMemberIdPB( uid: Int64.parseInt(userId), ); - await UserEventGetMemberInfo(payload).send().then((result) { result.fold( (member) { 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 c22559f21b..df6c1993a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart @@ -2,25 +2,55 @@ 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'; -/// A stream that receives answer events from an isolate or external process. -/// It caches events that might occur before a listener is attached. class AnswerStream { AnswerStream() { _port.handler = _controller.add; _subscription = _controller.stream.listen( - _handleEvent, - onDone: _onDoneCallback, - onError: _handleError, + (event) { + if (event.startsWith("data:")) { + _hasStarted = true; + final newText = event.substring(5); + _text += newText; + _onData?.call(_text); + } else if (event.startsWith("error:")) { + _error = event.substring(5); + _onError?.call(_error!); + } else if (event.startsWith("metadata:")) { + if (_onMetadata != null) { + final s = event.substring(9); + _onMetadata!(parseMetadata(s)); + } + } else if (event == "AI_RESPONSE_LIMIT") { + _aiLimitReached = true; + _onAIResponseLimit?.call(); + } else if (event == "AI_IMAGE_RESPONSE_LIMIT") { + _aiImageLimitReached = true; + _onAIImageResponseLimit?.call(); + } else if (event.startsWith("AI_MAX_REQUIRED:")) { + final msg = event.substring(16); + // If the callback is not registered yet, add the event to the buffer. + if (_onAIMaxRequired != null) { + _onAIMaxRequired!(msg); + } else { + _pendingAIMaxRequiredEvents.add(msg); + } + } + }, + onDone: () { + _onEnd?.call(); + }, + onError: (error) { + _error = error.toString(); + _onError?.call(error.toString()); + }, ); } final RawReceivePort _port = RawReceivePort(); final StreamController _controller = StreamController.broadcast(); late StreamSubscription _subscription; - bool _hasStarted = false; bool _aiLimitReached = false; bool _aiImageLimitReached = false; @@ -32,15 +62,13 @@ class AnswerStream { void Function()? _onStart; void Function()? _onEnd; void Function(String error)? _onError; - void Function()? _onLocalAIInitializing; void Function()? _onAIResponseLimit; void Function()? _onAIImageResponseLimit; void Function(String message)? _onAIMaxRequired; - void Function(MetadataCollection metadata)? _onMetadata; + void Function(MetadataCollection metadataCollection)? _onMetadata; - // Caches for events that occur before listen() is called. + // Buffer for events that occur before listen() is called. final List _pendingAIMaxRequiredEvents = []; - bool _pendingLocalAINotReady = false; int get nativePort => _port.sendPort.nativePort; bool get hasStarted => _hasStarted; @@ -49,61 +77,12 @@ class AnswerStream { String? get error => _error; String get text => _text; - /// Releases the resources used by the AnswerStream. Future dispose() async { await _controller.close(); await _subscription.cancel(); _port.close(); } - /// Handles incoming events from the underlying stream. - void _handleEvent(String event) { - if (event.startsWith(AIStreamEventPrefix.data)) { - _hasStarted = true; - final newText = event.substring(AIStreamEventPrefix.data.length); - _text += newText; - _onData?.call(_text); - } else if (event.startsWith(AIStreamEventPrefix.error)) { - _error = event.substring(AIStreamEventPrefix.error.length); - _onError?.call(_error!); - } else if (event.startsWith(AIStreamEventPrefix.metadata)) { - final s = event.substring(AIStreamEventPrefix.metadata.length); - _onMetadata?.call(parseMetadata(s)); - } else if (event == AIStreamEventPrefix.aiResponseLimit) { - _aiLimitReached = true; - _onAIResponseLimit?.call(); - } else if (event == AIStreamEventPrefix.aiImageResponseLimit) { - _aiImageLimitReached = true; - _onAIImageResponseLimit?.call(); - } 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(AIStreamEventPrefix.localAINotReady)) { - if (_onLocalAIInitializing != null) { - _onLocalAIInitializing!(); - } else { - _pendingLocalAINotReady = true; - } - } - } - - void _onDoneCallback() { - _onEnd?.call(); - } - - void _handleError(dynamic error) { - _error = error.toString(); - _onError?.call(_error!); - } - - /// Registers listeners for various events. - /// - /// If certain events have already occurred (e.g. AI_MAX_REQUIRED or LOCAL_AI_NOT_READY), - /// they will be flushed immediately. void listen({ void Function(String text)? onData, void Function()? onStart, @@ -113,7 +92,6 @@ class AnswerStream { void Function()? onAIImageResponseLimit, void Function(String message)? onAIMaxRequired, void Function(MetadataCollection metadata)? onMetadata, - void Function()? onLocalAIInitializing, }) { _onData = onData; _onStart = onStart; @@ -121,11 +99,10 @@ class AnswerStream { _onError = onError; _onAIResponseLimit = onAIResponseLimit; _onAIImageResponseLimit = onAIImageResponseLimit; - _onAIMaxRequired = onAIMaxRequired; _onMetadata = onMetadata; - _onLocalAIInitializing = onLocalAIInitializing; + _onAIMaxRequired = onAIMaxRequired; - // Flush pending AI_MAX_REQUIRED events. + // Flush any buffered AI_MAX_REQUIRED events. if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) { for (final msg in _pendingAIMaxRequiredEvents) { _onAIMaxRequired!(msg); @@ -133,12 +110,6 @@ class AnswerStream { _pendingAIMaxRequiredEvents.clear(); } - // Flush pending LOCAL_AI_NOT_READY event. - if (_pendingLocalAINotReady && _onLocalAIInitializing != null) { - _onLocalAIInitializing!(); - _pendingLocalAINotReady = false; - } - _onStart?.call(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart index 9977d1df72..90a2db168f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart @@ -19,17 +19,9 @@ class ChatSelectMessageBloc on( (event, emit) { event.when( - enableStartSelectingMessages: () { - emit(state.copyWith(enabled: true)); - }, toggleSelectingMessages: () { if (state.isSelectingMessages) { - emit( - state.copyWith( - isSelectingMessages: false, - selectedMessages: [], - ), - ); + emit(ChatSelectMessageState.initial()); } else { emit(state.copyWith(isSelectingMessages: true)); } @@ -58,13 +50,8 @@ class ChatSelectMessageBloc unselectAllMessages: () { emit(state.copyWith(selectedMessages: const [])); }, - reset: () { - emit( - state.copyWith( - isSelectingMessages: false, - selectedMessages: [], - ), - ); + saveAsPage: () { + emit(ChatSelectMessageState.initial()); }, ); }, @@ -83,8 +70,6 @@ class ChatSelectMessageBloc @freezed class ChatSelectMessageEvent with _$ChatSelectMessageEvent { - const factory ChatSelectMessageEvent.enableStartSelectingMessages() = - _EnableStartSelectingMessages; const factory ChatSelectMessageEvent.toggleSelectingMessages() = _ToggleSelectingMessages; const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) = @@ -94,7 +79,7 @@ class ChatSelectMessageEvent with _$ChatSelectMessageEvent { ) = _SelectAllMessages; const factory ChatSelectMessageEvent.unselectAllMessages() = _UnselectAllMessages; - const factory ChatSelectMessageEvent.reset() = _Reset; + const factory ChatSelectMessageEvent.saveAsPage() = _SaveAsPage; } @freezed @@ -102,11 +87,9 @@ class ChatSelectMessageState with _$ChatSelectMessageState { const factory ChatSelectMessageState({ required bool isSelectingMessages, required List selectedMessages, - required bool enabled, }) = _ChatSelectMessageState; factory ChatSelectMessageState.initial() => const ChatSelectMessageState( - enabled: false, isSelectingMessages: false, selectedMessages: [], ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index 76aba27dc0..1b3880e01d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -178,12 +178,8 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder customActions: [ CustomViewAction( view: notifier.view, - disabled: !state.enabled, leftIcon: FlowySvgs.ai_add_to_page_s, label: LocaleKeys.moreAction_saveAsNewPage.tr(), - tooltipMessage: state.enabled - ? null - : LocaleKeys.moreAction_saveAsNewPageDisabled.tr(), onTap: () { chatMessageSelectorBloc.add( const ChatSelectMessageEvent diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 90085354db..f7f22a3c93 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,6 +1,7 @@ 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'; @@ -8,8 +9,9 @@ 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'; import 'package:flutter_chat_core/flutter_chat_core.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart' @@ -48,14 +50,14 @@ class AIChatPage extends StatelessWidget { @override Widget build(BuildContext context) { - // if (userProfile.authenticator != AuthTypePB.Server) { - // return Center( - // child: FlowyText( - // LocaleKeys.chat_unsupportedCloudPrompt.tr(), - // fontSize: 20, - // ), - // ); - // } + if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { + return Center( + child: FlowyText( + LocaleKeys.chat_unsupportedCloudPrompt.tr(), + fontSize: 20, + ), + ); + } return MultiBlocProvider( providers: [ @@ -70,7 +72,6 @@ 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, @@ -91,29 +92,9 @@ class AIChatPage extends StatelessWidget { } } }, - child: FocusScope( - onKeyEvent: (focusNode, event) { - if (event is! KeyUpEvent) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.escape || - event.logicalKey == LogicalKeyboardKey.keyC && - HardwareKeyboard.instance.isControlPressed) { - final chatBloc = context.read(); - if (chatBloc.state.promptResponseState != - PromptResponseState.ready) { - chatBloc.add(ChatEvent.stopStream()); - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; - }, - child: _ChatContentPage( - view: view, - userProfile: userProfile, - ), + child: _ChatContentPage( + view: view, + userProfile: userProfile, ), ); }, @@ -262,16 +243,10 @@ class _ChatContentPage extends StatelessWidget { _onSelectMetadata(context, metadata), onRegenerate: () => context .read() - .add(ChatEvent.regenerateAnswer(message.id, null, null)), + .add(ChatEvent.regenerateAnswer(message.id, null)), onChangeFormat: (format) => context .read() - .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(), - ), + .add(ChatEvent.regenerateAnswer(message.id, format)), ); }, ); @@ -330,10 +305,6 @@ class _ChatContentPage extends StatelessWidget { ); } - context - .read() - .add(ChatSelectMessageEvent.enableStartSelectingMessages()); - return BlocSelector( selector: (state) => state.isSelectingMessages, builder: (context, isSelectingMessages) { @@ -355,56 +326,36 @@ class _ChatContentPage extends StatelessWidget { BuildContext context, ChatMessageRefSource metadata, ) async { - // When the source of metatdata is appflowy, which means it is a appflowy page - if (metadata.source == "appflowy") { + if (isURL(metadata.name)) { + late Uri uri; + try { + uri = Uri.parse(metadata.name); + // `Uri` identifies `localhost` as a scheme + if (!uri.hasScheme || uri.scheme == 'localhost') { + uri = Uri.parse("http://${metadata.name}"); + await InternetAddress.lookup(uri.host); + } + await launchUrl(uri); + } catch (err) { + Log.error("failed to open url $err"); + } + } else { final sidebarView = await ViewBackendService.getView(metadata.id).toNullable(); if (context.mounted) { openPageFromMessage(context, sidebarView); } - return; - } - - if (metadata.source == "web") { - if (isURL(metadata.name)) { - late Uri uri; - try { - uri = Uri.parse(metadata.name); - // `Uri` identifies `localhost` as a scheme - if (!uri.hasScheme || uri.scheme == 'localhost') { - uri = Uri.parse("http://${metadata.name}"); - await InternetAddress.lookup(uri.host); - } - await launchUrl(uri); - } catch (err) { - Log.error("failed to open url $err"); - } - } - return; } } } -class _Input extends StatefulWidget { +class _Input extends StatelessWidget { 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( @@ -434,7 +385,6 @@ class _InputState extends State<_Input> { 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 59b7fbd39b..d5ecd09c38 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.app_logo_s, + FlowySvgs.flowy_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 76d1af7134..4d5cd82098 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart @@ -41,7 +41,7 @@ class _MobileChatInputState extends State { void initState() { super.initState(); - textController.addListener(handleTextControllerChanged); + textController.addListener(handleTextControllerChange); // focusNode.onKeyEvent = handleKeyEvent; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -197,7 +197,7 @@ class _MobileChatInputState extends State { ); } - void handleTextControllerChanged() { + void handleTextControllerChange() { if (textController.value.isComposingRangeValid) { return; } @@ -268,8 +268,8 @@ class _MobileChatInputState extends State { focusedBorder: InputBorder.none, contentPadding: MobileAIPromptSizes.textFieldContentPadding, hintText: switch (state.aiType) { - AiType.cloud => LocaleKeys.chat_inputMessageHint.tr(), - AiType.local => LocaleKeys.chat_inputLocalAIMessageHint.tr() + AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(), + AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr() }, hintStyle: inputHintTextStyle(context), isCollapsed: true, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart index 790a3fac3c..4b25297d63 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart @@ -250,7 +250,7 @@ class _SaveToPageButtonState extends State { showSaveMessageSuccessToast(context, view); } - bloc.add(const ChatSelectMessageEvent.reset()); + bloc.add(const ChatSelectMessageEvent.saveAsPage()); return view; } @@ -275,7 +275,7 @@ class _SaveToPageButtonState extends State { showSaveMessageSuccessToast(context, newView); openPageFromMessage(context, newView); } - bloc.add(const ChatSelectMessageEvent.reset()); + bloc.add(const ChatSelectMessageEvent.saveAsPage()); } Future forceReload(String documentId) async { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index 2c09e77050..9e4cc603b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -21,35 +21,33 @@ class RelatedQuestionList extends StatelessWidget { @override Widget build(BuildContext context) { - return SelectionContainer.disabled( - child: ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: relatedQuestions.length + 1, - padding: - const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin, - separatorBuilder: (context, index) => const VSpace(4.0), - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.chat_relatedQuestion.tr(), - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w600, - ), - ); - } else { - return Align( - alignment: AlignmentDirectional.centerStart, - child: RelatedQuestionItem( - question: relatedQuestions[index - 1], - onQuestionSelected: onQuestionSelected, - ), - ); - } - }, - ), + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: relatedQuestions.length + 1, + padding: + const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin, + separatorBuilder: (context, index) => const VSpace(4.0), + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText( + LocaleKeys.chat_relatedQuestion.tr(), + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w600, + ), + ); + } else { + return Align( + alignment: AlignmentDirectional.centerStart, + child: RelatedQuestionItem( + question: relatedQuestions[index - 1], + onQuestionSelected: onQuestionSelected, + ), + ); + } + }, ); } } @@ -72,8 +70,7 @@ class RelatedQuestionItem extends StatelessWidget { child: FlowyText( question, lineHeight: 1.4, - maxLines: 2, - overflow: TextOverflow.ellipsis, + maxLines: null, ), ), expandText: false, 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 30dc918f70..d7a90bd18a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -46,7 +46,7 @@ class ChatWelcomePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const FlowySvg( - FlowySvgs.app_logo_xl, + FlowySvgs.flowy_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 deleted file mode 100644 index aa0d840574..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart +++ /dev/null @@ -1,145 +0,0 @@ -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_markdown_text.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart index 1e7d428263..e14275ca30 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart @@ -64,12 +64,8 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { super.didUpdateWidget(oldWidget); if (oldWidget.markdown != widget.markdown) { - final editorState = _parseMarkdown( - widget.markdown.trim(), - previousDocument: this.editorState.document, - ); - this.editorState.dispose(); - this.editorState = editorState; + editorState.dispose(); + editorState = _parseMarkdown(widget.markdown.trim()); scrollController.dispose(); scrollController = EditorScrollController( editorState: editorState, @@ -133,30 +129,8 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { ); } - EditorState _parseMarkdown( - String markdown, { - Document? previousDocument, - }) { - // merge the nodes from the previous document with the new document to keep the same node ids + EditorState _parseMarkdown(String markdown) { final document = customMarkdownToDocument(markdown); - final documentIterator = NodeIterator( - document: document, - startNode: document.root, - ); - if (previousDocument != null) { - final previousDocumentIterator = NodeIterator( - document: previousDocument, - startNode: previousDocument.root, - ); - while ( - documentIterator.moveNext() && previousDocumentIterator.moveNext()) { - final currentNode = documentIterator.current; - final previousNode = previousDocumentIterator.current; - if (currentNode.path.equals(previousNode.path)) { - currentNode.id = previousNode.id; - } - } - } final editorState = EditorState(document: document); return editorState; } diff --git a/frontend/appflowy_flutter/lib/plugins/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 08fd82188d..48a8459598 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -21,7 +21,6 @@ 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'; @@ -42,7 +41,6 @@ class AIMessageActionBar extends StatefulWidget { required this.showDecoration, this.onRegenerate, this.onChangeFormat, - this.onChangeModel, this.onOverrideVisibility, }); @@ -50,7 +48,6 @@ 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 @@ -129,12 +126,6 @@ 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, @@ -184,7 +175,8 @@ class CopyButton extends StatelessWidget { ); if (context.mounted) { showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), ); } }, @@ -268,11 +260,8 @@ class _ChangeFormatButtonState extends State { constraints: const BoxConstraints(), onClose: () => widget.onOverrideVisibility?.call(false), child: buildButton(context), - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: _ChangeFormatPopoverContent( - onRegenerate: widget.onRegenerate, - ), + popupBuilder: (_) => _ChangeFormatPopoverContent( + onRegenerate: widget.onRegenerate, ), ); } @@ -370,16 +359,11 @@ class _ChangeFormatPopoverContentState child: Row( mainAxisSize: MainAxisSize.min, children: [ - BlocBuilder( - builder: (context, state) { - return ChangeFormatBar( - spacing: 2.0, - showImageFormats: state.aiType.isCloud, - predefinedFormat: predefinedFormat, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); - }, - ); + ChangeFormatBar( + spacing: 2.0, + predefinedFormat: predefinedFormat, + onSelectPredefinedFormat: (format) { + setState(() => predefinedFormat = format); }, ), const HSpace(4.0), @@ -413,85 +397,6 @@ 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 2786799520..a938c9094f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart @@ -12,7 +12,6 @@ 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'; @@ -24,7 +23,6 @@ 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'; @@ -43,7 +41,6 @@ class ChatAIMessageBubble extends StatelessWidget { this.isSelectingMessages = false, this.onRegenerate, this.onChangeFormat, - this.onChangeModel, }); final Message message; @@ -53,7 +50,6 @@ 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) { @@ -77,7 +73,6 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, child: child, ); } @@ -87,7 +82,6 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, child: child, ); } @@ -97,7 +91,6 @@ class ChatAIMessageBubble extends StatelessWidget { message: message, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, child: child, ); } @@ -110,14 +103,12 @@ 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) { @@ -136,7 +127,6 @@ class ChatAIBottomInlineActions extends StatelessWidget { showDecoration: false, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, ), ), const VSpace(32.0), @@ -152,14 +142,12 @@ 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,23 +217,22 @@ class _ChatAIMessageHoverState extends State { setState(() => hoverActionBar = false); } }, - child: SizedBox( - width: 784, - height: DesktopAIChatSizes.messageActionBarIconSize + - DesktopAIChatSizes.messageHoverActionBarPadding.vertical, + child: Container( + constraints: BoxConstraints( + maxWidth: 784, + maxHeight: DesktopAIChatSizes.messageActionBarIconSize + + DesktopAIChatSizes + .messageHoverActionBarPadding.vertical, + ), child: hoverBubble || hoverActionBar || overrideVisibility - ? Align( - alignment: AlignmentDirectional.centerStart, - child: AIMessageActionBar( - message: widget.message, - showDecoration: true, - onRegenerate: widget.onRegenerate, - onChangeFormat: widget.onChangeFormat, - onChangeModel: widget.onChangeModel, - onOverrideVisibility: (visibility) { - overrideVisibility = visibility; - }, - ), + ? AIMessageActionBar( + message: widget.message, + showDecoration: true, + onRegenerate: widget.onRegenerate, + onChangeFormat: widget.onChangeFormat, + onOverrideVisibility: (visibility) { + overrideVisibility = visibility; + }, ) : null, ), @@ -315,14 +302,12 @@ 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) { @@ -343,8 +328,6 @@ class ChatAIMessagePopup extends StatelessWidget { _divider(), _changeFormatButton(context), _divider(), - _changeModelButton(context), - _divider(), _saveToPageButton(context), ], ); @@ -376,7 +359,8 @@ class ChatAIMessagePopup extends StatelessWidget { } if (context.mounted) { showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), ); } }, @@ -415,25 +399,6 @@ 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 380767105f..d6b3c87903 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -4,7 +4,6 @@ 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'; @@ -33,11 +32,9 @@ class ChatAIMessageWidget extends StatelessWidget { required this.questionId, required this.chatId, required this.refSourceJsonString, - required this.onStopStream, this.onSelectedMetadata, this.onRegenerate, this.onChangeFormat, - this.onChangeModel, this.isLastMessage = false, this.isStreaming = false, this.isSelectingMessages = false, @@ -53,9 +50,7 @@ class ChatAIMessageWidget extends StatelessWidget { final String? refSourceJsonString; final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; final void Function()? onRegenerate; - final void Function() onStopStream; final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; final bool isStreaming; final bool isLastMessage; final bool isSelectingMessages; @@ -113,13 +108,10 @@ class ChatAIMessageWidget extends StatelessWidget { isSelectingMessages: isSelectingMessages, onRegenerate: onRegenerate, onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - AIMarkdownText( - markdown: state.text, - ), + AIMarkdownText(markdown: state.text), if (state.sources.isNotEmpty) SelectionContainer.disabled( child: AIMessageMetadata( @@ -154,15 +146,6 @@ class ChatAIMessageWidget extends StatelessWidget { errorMessage: message, ); }, - onInitializingLocalAI: () { - onStopStream(); - - return ChatErrorMessageWidget( - errorMessage: LocaleKeys - .settings_aiPage_keys_localAIInitializing - .tr(), - ); - }, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart index 652fe3791b..1b0084c77c 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,6 +14,7 @@ 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, ); @@ -35,6 +36,7 @@ void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) { return; } showToastNotification( + context, richMessage: TextSpan( children: [ TextSpan( diff --git a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart index ebda487515..b25bb5af06 100644 --- a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart +++ b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart @@ -36,7 +36,7 @@ class BlankPagePlugin extends Plugin { PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder(); @override - PluginId get id => ""; + PluginId get id => "BlankStack"; @override PluginType get pluginType => PluginType.blank; 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 73b2d2977b..df159b817b 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,6 +47,15 @@ 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 ec789b03a0..70c5e074ab 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,11 +143,12 @@ class RelationCellBloc extends Bloc { (f) => null, ); if (databaseMeta != null) { - final result = await ViewBackendService.getView(databaseMeta.viewId); + final result = + await ViewBackendService.getView(databaseMeta.inlineViewId); return result.fold( (s) => DatabaseMeta( databaseId: databaseId, - viewId: databaseMeta.viewId, + inlineViewId: databaseMeta.inlineViewId, 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 c6e4e6484b..f8ed915b62 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,11 +241,6 @@ 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 93fd69bcfc..8370bd9bff 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,28 +411,23 @@ class FieldController { /// Listen for field setting changes in the backend. void _listenOnFieldSettingsChanged() { FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) { - final newFields = [...fieldInfos]; + final List newFields = fieldInfos; + var updatedField = newFields.firstOrNull; - if (newFields.isEmpty) { + if (updatedField == null) { return null; } final index = newFields .indexWhere((field) => field.id == updatedFieldSettings.fieldId); - if (index != -1) { newFields[index] = newFields[index].copyWith(fieldSettings: updatedFieldSettings); - _fieldNotifier.fieldInfos = newFields; - _fieldSettings - ..removeWhere( - (field) => field.fieldId == updatedFieldSettings.fieldId, - ) - ..add(updatedFieldSettings); - return newFields[index]; + updatedField = newFields[index]; } - return null; + _fieldNotifier.fieldInfos = newFields; + return updatedField; } _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 4ddde80b79..691b6b7227 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.viewId).then( + return ViewBackendService.getView(meta.inlineViewId).then( (result) => result.fold( (s) => DatabaseMeta( databaseId: meta.databaseId, - viewId: meta.viewId, + inlineViewId: meta.inlineViewId, databaseName: s.name, ), (f) => null, @@ -43,10 +43,10 @@ class DatabaseMeta with _$DatabaseMeta { /// id of the database required String databaseId, - /// id of the view - required String viewId, + /// id of the inline view + required String inlineViewId, - /// name of the database + /// name of the database, currently identical to the name of the inline view 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 f735618dd8..4f975cd1a6 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,24 +73,27 @@ 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 viewId = await DatabaseEventGetDefaultDatabaseViewId( - DatabaseIdPB(value: databaseId), - ).send().fold( - (pb) => pb.value, - (error) => null, - ); - - if (viewId == null) { + final databaseMeta = + await DatabaseEventGetDatabaseMeta(DatabaseIdPB(value: databaseId)) + .send() + .fold((s) => s, (f) => null); + if (databaseMeta == null) { return; } - - final databaseView = await ViewBackendService.getView(viewId) - .fold((viewPB) => viewPB, (f) => null); - if (databaseView == null) { + final inlineView = + await ViewBackendService.getView(databaseMeta.inlineViewId) + .fold((viewPB) => viewPB, (f) => null); + if (inlineView == null) { return; } - final databaseController = DatabaseController(view: databaseView); + final databaseController = DatabaseController(view: inlineView); await databaseController.open().fold( (s) => databaseController.setIsLoading(false), (f) => null, @@ -101,7 +104,7 @@ class RelatedRowDetailPageBloc } final rowController = RowController( rowMeta: rowInfo.rowMeta, - viewId: databaseView.id, + viewId: inlineView.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 351dea2cd8..ae0b9173c7 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?.workspaceAuthType == AuthTypePB.Server && - databaseId != null, + shouldShowIndicator: userProfile?.authenticator == + AuthenticatorPB.AppFlowyCloud && + 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 70d00bcd25..77a26a9c58 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -386,15 +386,15 @@ class _BoardContentState extends State<_BoardContent> { scrollManager: scrollManager, ), ), - cardBuilder: (cardContext, column, columnItem) => + cardBuilder: (context, column, columnItem) => MultiBlocProvider( key: ValueKey("board_card_${column.id}_${columnItem.id}"), providers: [ BlocProvider.value( - value: cardContext.read(), + value: context.read(), ), BlocProvider.value( - value: cardContext.read(), + value: context.read(), ), BlocProvider( create: (_) => ViewLockStatusBloc(view: widget.view) @@ -402,7 +402,7 @@ class _BoardContentState extends State<_BoardContent> { ), ], child: BlocBuilder( - builder: (lockStatusContext, state) { + builder: (context, state) { return IgnorePointer( ignoring: state.isLocked, child: _BoardCard( @@ -412,13 +412,6 @@ class _BoardContentState extends State<_BoardContent> { notifier: widget.focusScope, cellBuilder: cellBuilder, compactMode: compactMode, - onOpenCard: (rowMeta) => _openCard( - context: context, - databaseController: lockStatusContext - .read() - .databaseController, - rowMeta: rowMeta, - ), ), ); }, @@ -588,7 +581,6 @@ class _BoardCard extends StatefulWidget { required this.cellBuilder, required this.notifier, required this.compactMode, - required this.onOpenCard, }); final AppFlowyGroupData afGroupData; @@ -597,7 +589,6 @@ class _BoardCard extends StatefulWidget { final CardCellBuilder cellBuilder; final BoardFocusScope notifier; final bool compactMode; - final void Function(RowMetaPB) onOpenCard; @override State<_BoardCard> createState() => _BoardCardState(); @@ -707,8 +698,10 @@ class _BoardCardState extends State<_BoardCard> { groupingFieldId: widget.groupItem.fieldInfo.id, isEditing: _isEditing, cellBuilder: widget.cellBuilder, - onTap: (context) => widget.onOpenCard( - context.read().rowController.rowMeta, + onTap: (context) => _openCard( + context: context, + databaseController: databaseController, + rowMeta: context.read().rowController.rowMeta, ), onShiftTap: (_) { Focus.of(context).requestFocus(); @@ -854,7 +847,7 @@ class _BoardTrailingState extends State { suffixIcon: Padding( padding: const EdgeInsets.only(left: 4, bottom: 8.0), child: FlowyIconButton( - icon: const FlowySvg(FlowySvgs.close_filled_s), + icon: const FlowySvg(FlowySvgs.close_filled_m), hoverColor: Colors.transparent, onPressed: () => _textController.clear(), ), 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 915bf70a61..23c2fe1f91 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -108,7 +108,7 @@ class _GridFieldCellState extends State { top: 0, bottom: 0, right: 0, - child: DragToExpandLine(), + child: _DragToExpandLine(), ); return _GridHeaderCellContainer( @@ -158,11 +158,8 @@ class _GridHeaderCellContainer extends StatelessWidget { } } -@visibleForTesting -class DragToExpandLine extends StatelessWidget { - const DragToExpandLine({ - super.key, - }); +class _DragToExpandLine extends StatelessWidget { + const _DragToExpandLine(); @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 7c2dc40869..3554f9112e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -362,13 +362,9 @@ const kDatabasePluginWidgetBuilderActionBuilder = 'action_builder'; const kDatabasePluginWidgetBuilderNode = 'node'; class DatabasePluginWidgetBuilderSize { - const DatabasePluginWidgetBuilderSize({ - required this.horizontalPadding, - this.verticalPadding = 16.0, - }); + const DatabasePluginWidgetBuilderSize({required this.horizontalPadding}); 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 ab0533819a..b10d63d2d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -1,3 +1,5 @@ +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'; @@ -14,7 +16,6 @@ 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'; @@ -200,16 +201,19 @@ class _ChecklistItems extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), proxyDecorator: (child, index, _) => Material( color: Colors.transparent, - child: MouseRegion( - cursor: UniversalPlatform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: IgnorePointer( - child: BlocProvider.value( + child: Stack( + children: [ + 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 39616dbcf8..0dc7779e55 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart @@ -203,7 +203,7 @@ class MobileURLEditor extends StatelessWidget { ClipboardData(text: textEditingController.text), ); Fluttertoast.showToast( - msg: LocaleKeys.message_copy_success.tr(), + msg: LocaleKeys.grid_url_copiedNotification.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 9853f9c1bd..b788d6bd38 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,7 +14,6 @@ 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'; @@ -126,16 +125,19 @@ class ChecklistItemList extends StatelessWidget { shrinkWrap: true, proxyDecorator: (child, index, _) => Material( color: Colors.transparent, - child: MouseRegion( - cursor: UniversalPlatform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: IgnorePointer( - child: BlocProvider.value( + child: Stack( + children: [ + 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 7f6960de9d..e68e77cd97 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -256,7 +256,7 @@ class _CellEditorTitle extends StatelessWidget { } void _openRelatedDatbase(BuildContext context) { - FolderEventGetView(ViewIdPB(value: databaseMeta.viewId)) + FolderEventGetView(ViewIdPB(value: databaseMeta.inlineViewId)) .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 debbb467e7..8d64c537c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -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,8 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); late final isLocalMode = - (widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) == - AuthTypePB.Local; + (widget.userProfile?.authenticator ?? AuthenticatorPB.Local) == + AuthenticatorPB.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 436dbd085d..0489db8907 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -5,12 +5,10 @@ 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'; import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -73,16 +71,9 @@ class _RowEditor extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => DocumentBloc(documentId: view.id) - ..add(const DocumentEvent.initial()), - ), - BlocProvider( - create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), - ), - ], + return BlocProvider( + create: (_) => + DocumentBloc(documentId: view.id)..add(const DocumentEvent.initial()), child: BlocConsumer( listenWhen: (previous, current) => previous.isDocumentEmpty != current.isDocumentEmpty, @@ -121,34 +112,29 @@ class _RowEditor extends StatelessWidget { return context; }, dispose: (_, editorContext) => editorContext.dispose(), - child: AiWriterScrollWrapper( + child: EditorDropHandler( viewId: view.id, editorState: editorState, - child: EditorDropHandler( + isLocalMode: context.read().isLocalMode, + dropManagerState: context.read(), + child: EditorTransactionService( viewId: view.id, editorState: editorState, - isLocalMode: context.read().isLocalMode, - dropManagerState: context.read(), - child: EditorTransactionService( - viewId: view.id, - editorState: editorState, - child: Provider( - create: (context) => DatabasePluginWidgetBuilderSize( - horizontalPadding: 0, - ), - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: const EdgeInsets.only(left: 16, right: 54), - ), - showParagraphPlaceholder: (editorState, _) => - editorState.document.isEmpty, - placeholderText: (_) => - LocaleKeys.cardDetails_notesPlaceholder.tr(), + 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(), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart index c7bc286371..5f40959c02 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart @@ -24,11 +24,11 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { case DatabaseSettingAction.showProperties: return FlowySvgs.multiselect_s; case DatabaseSettingAction.showLayout: - return FlowySvgs.database_layout_s; + return FlowySvgs.database_layout_m; case DatabaseSettingAction.showGroup: return FlowySvgs.group_s; case DatabaseSettingAction.showCalendarLayout: - return FlowySvgs.calendar_layout_s; + return FlowySvgs.calendar_layout_m; } } 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 ee52be8c26..eaa82b22e9 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,4 +1,3 @@ -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'; @@ -8,10 +7,8 @@ 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'; @@ -21,12 +18,8 @@ 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'; // This widget is largely copied from `plugins/document/document_page.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. @@ -53,6 +46,18 @@ 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( @@ -67,10 +72,6 @@ class _DatabaseDocumentPageState extends State { documentId: widget.documentId, )..add(const DocumentEvent.initial()), ), - BlocProvider( - create: (_) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), - ), ], child: BlocBuilder( builder: (context, state) { @@ -97,11 +98,7 @@ class _DatabaseDocumentPageState extends State { return BlocListener( listener: _onNotificationAction, listenWhen: (_, curr) => curr.action != null, - child: AiWriterScrollWrapper( - viewId: widget.view.id, - editorState: editorState, - child: _buildEditorPage(context, state), - ), + child: _buildEditorPage(context, state), ); }, ), @@ -118,34 +115,21 @@ 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 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), - ], - ), + return EditorTransactionService( + viewId: widget.view.id, + editorState: state.editorState!, + child: Column( + children: [ + if (state.isDeleted) _buildBanner(context), + Expanded(child: appflowyEditorPage), + ], ), ); } @@ -218,6 +202,20 @@ 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 264ec4bb11..268863664b 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?.workspaceAuthType ?? AuthTypePB.Local; - return type == AuthTypePB.Local; + final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local; + return type == AuthenticatorPB.Local; } @override @@ -272,14 +272,12 @@ class DocumentBloc extends Bloc { } if (options.inMemoryUpdate) { - if (enableDocumentInternalLog) { - Log.trace('skip transaction for in-memory update'); - } + Log.info('skip transaction for in-memory update'); return; } if (enableDocumentInternalLog) { - Log.trace( + Log.debug( '[TransactionAdapter] 1. transaction before apply: ${transaction.hashCode}', ); } @@ -291,7 +289,7 @@ class DocumentBloc extends Bloc { await _documentRules.applyRules(value: value); if (enableDocumentInternalLog) { - Log.trace( + Log.debug( '[TransactionAdapter] 4. transaction after apply: ${transaction.hashCode}', ); } @@ -442,6 +440,7 @@ 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 a0678372cf..74a6199b89 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc emit( state.copyWith( shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, ), ); final deviceId = ApplicationInfo.deviceId; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index 38bf2bcd14..574ae34af8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -13,6 +13,7 @@ import 'package:appflowy_editor/appflowy_editor.dart' NodeIterator, NodeExternalValues, HeadingBlockKeys, + QuoteBlockKeys, NumberedListBlockKeys, BulletedListBlockKeys, blockComponentDelta; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart index f530b1ef8d..230a5b8fa7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart @@ -104,28 +104,6 @@ class DocumentRules { } else { // otherwise, delete the column deleteColumnsTransaction.deleteNode(column); - - final deletedColumnRatio = - column.attributes[SimpleColumnBlockKeys.ratio]; - if (deletedColumnRatio != null) { - // update the ratio of the columns - final columnsNode = column.columnsParent; - if (columnsNode != null) { - final length = columnsNode.children.length; - for (final columnNode in columnsNode.children) { - final ratio = - columnNode.attributes[SimpleColumnBlockKeys.ratio] ?? - 1.0 / length; - if (ratio != null) { - deleteColumnsTransaction.updateNode(columnNode, { - ...columnNode.attributes, - SimpleColumnBlockKeys.ratio: - ratio + deletedColumnRatio / (length - 1), - }); - } - } - } - } } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 7254539809..0fae90920d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, ), ); _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 8716bb7ae2..5c695fa508 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -5,7 +5,6 @@ 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'; @@ -89,12 +88,9 @@ class _DocumentPageState extends State BlocProvider.value(value: documentBloc), BlocProvider.value( value: ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - BlocProvider( - create: (context) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), - lazy: false, + ..add( + ViewLockStatusEvent.initial(), + ), ), ], child: BlocConsumer( @@ -128,20 +124,14 @@ class _DocumentPageState extends State return const SizedBox.shrink(); } - 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, + return BlocListener( + listener: (context, state) { + editorState.editable = !state.isLocked; + }, + child: + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: onNotificationAction, child: buildEditorPage(context, state), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5e7eefc24e..7e3b5347da 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -6,8 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mo import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; @@ -16,7 +15,6 @@ 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. @@ -38,7 +36,6 @@ final Set supportSlashMenuNodeTypes = { NumberedListBlockKeys.type, QuoteBlockKeys.type, ToggleListBlockKeys.type, - CalloutBlockKeys.type, // Simple table SimpleTableBlockKeys.type, @@ -124,19 +121,10 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) { }, indentPadding: (node, textDirection) { double padding = 26.0; - // only add indent padding for the top level node to align the children - if (UniversalPlatform.isMobile && node.level == 1) { - padding += EditorStyleCustomizer.nodeHorizontalPadding - 4; + if (UniversalPlatform.isMobile && node.path.length == 1) { + padding += EditorStyleCustomizer.nodeHorizontalPadding; } - - // in the quote block, we reduce the indent padding for the first level block. - // So we have to add more padding for the second level to avoid the drag menu overlay the quote icon. - if (node.parent?.type == QuoteBlockKeys.type && - UniversalPlatform.isDesktop) { - padding += 22; - } - return textDirection == TextDirection.ltr ? EdgeInsets.only(left: padding) : EdgeInsets.only(right: padding); @@ -215,16 +203,6 @@ void _customBlockOptionActions( ), ); - builder.actionTrailingBuilder = (context, state) { - if (context.node.parent?.type == QuoteBlockKeys.type) { - return const SizedBox( - width: 24, - height: 24, - ); - } - return const SizedBox.shrink(); - }; - builder.actionBuilder = (context, state) { double top = builder.configuration.padding(context.node).top; final type = context.node.type; @@ -614,19 +592,6 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( node: node, configuration: configuration, ), - indentPadding: (node, textDirection) { - if (UniversalPlatform.isMobile) { - return configuration.indentPadding(node, textDirection); - } - - if (node.isInTable) { - return textDirection == TextDirection.ltr - ? EdgeInsets.only(left: 24) - : EdgeInsets.only(right: 24); - } - - return EdgeInsets.zero; - }, ), ); } @@ -833,14 +798,8 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder( configuration: configuration, textSpan: textSpan, ), - indentPadding: (node, _) => EdgeInsets.only(left: 42), ), - inlinePadding: (node) { - if (node.children.isEmpty) { - return const EdgeInsets.symmetric(vertical: 8.0); - } - return EdgeInsets.only(top: 8.0, bottom: 2.0); - }, + inlinePadding: const EdgeInsets.symmetric(vertical: 8.0), defaultColor: calloutBGColor, ); } @@ -970,11 +929,11 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder( ); } -CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( +LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( BuildContext context, BlockComponentConfiguration configuration, ) { - return CustomLinkPreviewBlockComponentBuilder( + return LinkPreviewBlockComponentBuilder( configuration: configuration.copyWith( padding: (node) { if (UniversalPlatform.isMobile) { @@ -983,6 +942,21 @@ CustomLinkPreviewBlockComponentBuilder _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, + ), ); } @@ -1050,11 +1024,6 @@ TextStyle _buildTextStyleInTableCell( }) { TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan); - textStyle = textStyle.copyWith( - fontFamily: textSpan?.style?.fontFamily, - fontSize: textSpan?.style?.fontSize, - ); - if (node.isInHeaderColumn || node.isInHeaderRow || node.isInBoldColumn || diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index edb19232be..2f2b2b0aa2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -15,12 +15,10 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; @@ -28,17 +26,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; -import 'editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart'; -import 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/text_heading_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; - /// Wrapper for the appflowy editor. class AppFlowyEditorPage extends StatefulWidget { const AppFlowyEditorPage({ @@ -94,25 +81,23 @@ class _AppFlowyEditorPageState extends State ]; final List toolbarItems = [ - improveWritingItem, - group0PaddingItem, - aiWriterItem, - customTextHeadingItem, - buildPaddingPlaceholderItem( - 1, - isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, - ), - ...customMarkdownFormatItems, - group1PaddingItem, - customTextColorItem, - group1PaddingItem, - customHighlightColorItem, - customInlineCodeItem, - suggestionsItem, - customLinkItem, - group4PaddingItem, - customTextAlignItem, - moreOptionItem, + improveWritingItem..isActive = onlyShowInTextTypeAndExcludeTable, + aiWriterItem..isActive = onlyShowInTextTypeAndExcludeTable, + paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + headingsToolbarItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + ...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType), + quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + bulletedListItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + numberedListItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + inlineMathEquationItem, + linkItem, + alignToolbarItem, + buildTextColorItem()..isActive = showInAnyTextType, + buildHighlightColorItem()..isActive = showInAnyTextType, + customizeFontToolbarItem..isActive = showInAnyTextType, ]; List get characterShortcutEvents { @@ -169,15 +154,10 @@ class _AppFlowyEditorPageState extends State InlineMathEquationKeys.formula, ]); - indentableBlockTypes.addAll([ - ToggleListBlockKeys.type, - CalloutBlockKeys.type, - QuoteBlockKeys.type, - ]); + indentableBlockTypes.add(ToggleListBlockKeys.type); convertibleBlockTypes.addAll([ ToggleListBlockKeys.type, CalloutBlockKeys.type, - QuoteBlockKeys.type, ]); editorLaunchUrl = (url) { @@ -351,7 +331,6 @@ class _AppFlowyEditorPageState extends State final isViewDeleted = context.read().state.isDeleted; final isLocked = context.read()?.state.isLocked ?? false; - final editor = Directionality( textDirection: textDirection, child: AppFlowyEditor( @@ -384,9 +363,7 @@ class _AppFlowyEditorPageState extends State contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, - autoScrollEdgeOffset: UniversalPlatform.isDesktopOrWeb - ? 250 - : appFlowyEditorAutoScrollEdgeOffset, + footer: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { @@ -395,7 +372,7 @@ class _AppFlowyEditorPageState extends State }, child: SizedBox( width: double.infinity, - height: UniversalPlatform.isDesktopOrWeb ? 600 : 400, + height: UniversalPlatform.isDesktopOrWeb ? 200 : 400, ), ), dropTargetStyle: AppFlowyDropTargetStyle( @@ -425,51 +402,26 @@ class _AppFlowyEditorPageState extends State anchor: anchor, closeToolbar: closeToolbar, ), - floatingToolbarHeight: 32, child: editor, ), ); } - final appTheme = AppFlowyTheme.of(context); + return Center( - 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, - editorScrollController: editorScrollController, - textDirection: textDirection, - tooltipBuilder: (context, id, message, child) => - widget.styleCustomizer.buildToolbarItemTooltip( - context, - id, - message, - child, - ), - child: editor, + child: FloatingToolbar( + style: styleCustomizer.floatingToolbarStyleBuilder(), + items: toolbarItems, + editorState: editorState, + editorScrollController: editorScrollController, + textDirection: textDirection, + tooltipBuilder: (context, id, message, child) => + widget.styleCustomizer.buildToolbarItemTooltip( + context, + id, + message, + child, ), + child: editor, ), ); } @@ -480,13 +432,11 @@ class _AppFlowyEditorPageState extends State }) { final documentBloc = context.read(); final isLocalMode = documentBloc.isLocalMode; - final view = context.read().state.view; return slashMenuItemsBuilder( editorState: editorState, node: node, isLocalMode: isLocalMode, documentBloc: documentBloc, - view: view, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index abed98136d..52b518a718 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -10,8 +10,7 @@ import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class BlockActionOptionState {} @@ -259,13 +258,11 @@ class BlockActionOptionCubit extends Cubit { emit(BlockActionOptionState()); // Emit a new state to trigger UI update } - static Future turnIntoBlock( + Future turnIntoBlock( String type, - Node node, - EditorState editorState, { + Node node, { int? level, String? currentViewId, - bool keepSelection = false, }) async { final selection = editorState.selection; if (selection == null) { @@ -289,8 +286,6 @@ class BlockActionOptionCubit extends Cubit { type: toType, selectedNodes: selectedNodes, level: level, - editorState: editorState, - afterSelection: keepSelection ? selection : null, )) { return true; } @@ -302,7 +297,6 @@ class BlockActionOptionCubit extends Cubit { selectedNodes: selectedNodes, selection: selection, currentViewId: currentViewId, - editorState: editorState, )) { return true; } @@ -327,8 +321,9 @@ class BlockActionOptionCubit extends Cubit { }, ); - // heading block should not have children - if ([HeadingBlockKeys.type].contains(toType)) { + // heading block and callout block should not have children + if ([HeadingBlockKeys.type, CalloutBlockKeys.type, QuoteBlockKeys.type] + .contains(toType)) { afterNode = afterNode.copyWith(children: []); afterNode = await _handleSubPageNode(afterNode, node); insertedNode.add(afterNode); @@ -349,7 +344,6 @@ class BlockActionOptionCubit extends Cubit { insertedNode, ); transaction.deleteNodes(selectedNodes); - if (keepSelection) transaction.afterSelection = selection; await editorState.apply(transaction); return true; @@ -359,7 +353,7 @@ class BlockActionOptionCubit extends Cubit { /// /// Returns the altered [Node] with the delta as the Views' name. /// - static Future _handleSubPageNode(Node node, Node subPageNode) async { + Future _handleSubPageNode(Node node, Node subPageNode) async { if (subPageNode.type != SubPageBlockKeys.type) { return node; } @@ -376,7 +370,7 @@ class BlockActionOptionCubit extends Cubit { /// Returns the [Delta] from a SubPage [Node], where the /// [Delta] is the views' name. /// - static Future _deltaFromSubPageNode(Node node) async { + Future _deltaFromSubPageNode(Node node) async { if (node.type != SubPageBlockKeys.type) { return null; } @@ -406,10 +400,9 @@ class BlockActionOptionCubit extends Cubit { // - paragraph 1 // - paragraph 2 // when turning "Toggle Heading 1" into toggle heading, the bulleted items will be moved into the toggle heading - static Future turnIntoSingleToggleHeading({ + Future turnIntoSingleToggleHeading({ required String type, required List selectedNodes, - required EditorState editorState, int? level, Delta? delta, Selection? afterSelection, @@ -469,7 +462,7 @@ class BlockActionOptionCubit extends Cubit { blockComponentDelta: newDelta.toJson(), }, children: [ - ...node.children.map((e) => e.deepCopy()), + ...node.children, ...insertedNodes.map((e) => e.deepCopy()), ], ); @@ -499,12 +492,11 @@ class BlockActionOptionCubit extends Cubit { return true; } - static Future turnIntoPage({ + Future turnIntoPage({ required String type, required List selectedNodes, required Selection selection, required String currentViewId, - required EditorState editorState, }) async { if (type != SubPageBlockKeys.type || selectedNodes.isEmpty) { return false; @@ -560,7 +552,7 @@ class BlockActionOptionCubit extends Cubit { return true; } - static Future _extractNameFromNodes(List? nodes) async { + Future _extractNameFromNodes(List? nodes) async { if (nodes == null || nodes.isEmpty) { return ''; } @@ -610,7 +602,7 @@ class BlockActionOptionCubit extends Cubit { return name.substring(0, name.length > 30 ? 30 : name.length); } - static List _extractChildViewIds(List nodes) { + List _extractChildViewIds(List nodes) { final List viewIds = []; for (final node in nodes) { if (node.type == SubPageBlockKeys.type) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart index 7bc1fba8d9..f09781591f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart @@ -89,7 +89,7 @@ class _DraggableOptionButtonState extends State { interceptor: (context, targetNode) { // if the cursor node is in a columns block or a column block, // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.columnParent; + final parentColumnNode = targetNode.parentColumn; if (parentColumnNode != null) { final position = getDragAreaPosition( context, @@ -147,7 +147,7 @@ class _DraggableOptionButtonState extends State { interceptor: (context, targetNode) { // if the cursor node is in a columns block or a column block, // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.columnParent; + final parentColumnNode = targetNode.parentColumn; if (parentColumnNode != null) { final position = getDragAreaPosition( context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index ca99491b94..2e56331faf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -1,7 +1,7 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -59,37 +59,38 @@ Future dragToMoveNode( // 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent // 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node final transaction = editorState.transaction; - final targetNodeParent = targetNode.columnsParent; - + final targetNodeParent = targetNode.parentColumnsBlock; if (targetNodeParent != null) { final length = targetNodeParent.children.length; - final ratios = targetNodeParent.children - .map( - (e) => - e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length, - ) - .map((e) => e * length / (length + 1)) - .toList(); - final columnNode = simpleColumnNode( children: [node.deepCopy()], - ratio: 1.0 / (length + 1), + width: (node.rect.width * 1 / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), ); - for (final (index, column) in targetNodeParent.children.indexed) { + + for (final column in targetNodeParent.children) { + final width = + column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; transaction.updateNode(column, { ...column.attributes, - SimpleColumnBlockKeys.ratio: ratios[index], + SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), }); } transaction.insertNode(targetNode.path.next, columnNode); transaction.deleteNode(node); } else { + final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), - simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), + simpleColumnNode(children: [targetNode.deepCopy()], width: width), + simpleColumnNode(children: [node.deepCopy()], width: width), ], ); @@ -107,37 +108,39 @@ Future dragToMoveNode( // 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent // 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node final transaction = editorState.transaction; - final targetNodeParent = targetNode.columnsParent; + final targetNodeParent = targetNode.parentColumnsBlock; if (targetNodeParent != null) { // find the previous sibling node of the target node final length = targetNodeParent.children.length; - final ratios = targetNodeParent.children - .map( - (e) => - e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length, - ) - .map((e) => e * length / (length + 1)) - .toList(); final columnNode = simpleColumnNode( children: [node.deepCopy()], - ratio: 1.0 / (length + 1), + width: (node.rect.width * 1 / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), ); - for (final (index, column) in targetNodeParent.children.indexed) { + for (final column in targetNodeParent.children) { + final width = + column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; transaction.updateNode(column, { ...column.attributes, - SimpleColumnBlockKeys.ratio: ratios[index], + SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), }); } transaction.insertNode(targetNode.path.previous, columnNode); transaction.deleteNode(node); } else { + final width = targetNode.rect.width / 2 - 16; final columnsNode = simpleColumnsNode( children: [ - simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), - simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), + simpleColumnNode(children: [node.deepCopy()], width: width), + simpleColumnNode(children: [targetNode.deepCopy()], width: width), ], ); @@ -188,6 +191,7 @@ Future dragToMoveNode( Node dragTargetNode, Offset dragOffset, ) { + debugPrint('getDragAreaPosition - dragTargetNode: ${dragTargetNode.type}'); final selectable = dragTargetNode.selectable; final renderBox = selectable?.context.findRenderObject() as RenderBox?; if (selectable == null || renderBox == null) { @@ -246,6 +250,10 @@ Future dragToMoveNode( verticalPosition = VerticalPosition.bottom; } + debugPrint( + 'verticalPosition: $verticalPosition, horizontalPosition: $horizontalPosition', + ); + return (verticalPosition, horizontalPosition, globalBlockRect); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart index 571cb4baa0..14ec6773a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart @@ -1,8 +1,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/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart index c927fcf85f..ac3774a511 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,10 +2,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -148,134 +148,213 @@ 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, - 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, - ); - }), - ], + children: _buildTurnIntoOptions(context, node), ); } - 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, - ), - ), - ); - } + List _buildTurnIntoOptions(BuildContext context, Node node) { + final children = []; - 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), - ), - ); + 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: () => context.read().turnIntoBlock( + type, + node, + level: level, + currentViewId: getIt().latestOpenView?.id, + ), + ); + } + + 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, + ); + } + + FlowySvgData _buildLeftIcon(String type, {int? level}) { + if (type == ParagraphBlockKeys.type) { + return FlowySvgs.slash_menu_icon_text_s; + } else if (type == HeadingBlockKeys.type) { + switch (level) { + case 1: + return FlowySvgs.slash_menu_icon_h1_s; + case 2: + return FlowySvgs.slash_menu_icon_h2_s; + case 3: + return FlowySvgs.slash_menu_icon_h3_s; + default: + return FlowySvgs.slash_menu_icon_text_s; + } + } else if (type == QuoteBlockKeys.type) { + return FlowySvgs.slash_menu_icon_quote_s; + } else if (type == BulletedListBlockKeys.type) { + return FlowySvgs.slash_menu_icon_bulleted_list_s; + } else if (type == NumberedListBlockKeys.type) { + return FlowySvgs.slash_menu_icon_numbered_list_s; + } else if (type == TodoListBlockKeys.type) { + return FlowySvgs.slash_menu_icon_checkbox_s; + } else if (type == CalloutBlockKeys.type) { + return FlowySvgs.slash_menu_icon_callout_s; + } else if (type == SubPageBlockKeys.type) { + return FlowySvgs.icon_document_s; + } else if (type == ToggleListBlockKeys.type) { + switch (level) { + case 1: + return FlowySvgs.toggle_heading1_s; + case 2: + return FlowySvgs.toggle_heading2_s; + case 3: + return FlowySvgs.toggle_heading3_s; + default: + return FlowySvgs.slash_menu_icon_toggle_s; + } + } + + 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.document_slashMenu_name_bulletedList.tr(); + case NumberedListBlockKeys.type: + return LocaleKeys.document_slashMenu_name_numberedList.tr(); + case TodoListBlockKeys.type: + return LocaleKeys.document_slashMenu_name_todoList.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.document_slashMenu_name_toggleHeading1.tr(); + case 2: + return LocaleKeys.document_slashMenu_name_toggleHeading2.tr(); + case 3: + return LocaleKeys.document_slashMenu_name_toggleHeading3.tr(); + default: + return LocaleKeys.document_slashMenu_name_toggleList.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 f78f7d35fd..81a52adb69 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart @@ -2,11 +2,13 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,8 +17,7 @@ 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 'widgets/ai_writer_suggestion_actions.dart'; -import 'widgets/ai_writer_prompt_input_more_button.dart'; +import 'suggestion_action_bar.dart'; class AiWriterBlockKeys { const AiWriterBlockKeys._(); @@ -65,10 +66,6 @@ class AIWriterBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -86,7 +83,6 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -95,12 +91,19 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget { } class _AIWriterBlockComponentState extends State { + final key = GlobalKey(); final textController = TextEditingController(); + final textFieldFocusNode = FocusNode(); 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() { @@ -108,14 +111,18 @@ class _AIWriterBlockComponentState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { overlayController.show(); - context.read().register(widget.node); + textFieldFocusNode.requestFocus(); + if (!widget.node.isAiWriterInitialized) { + aiWriterCubit.init(); + } }); } @override void dispose() { textController.dispose(); - focusNode.dispose(); + textFieldFocusNode.dispose(); + aiWriterCubit.close(); super.dispose(); } @@ -125,49 +132,71 @@ class _AIWriterBlockComponentState extends State { return const SizedBox.shrink(); } - final documentId = context.read()?.documentId; - - return BlocProvider( - create: (_) => AIPromptInputBloc( - predefinedFormat: null, - objectId: documentId ?? editorState.document.root.id, - ), + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: aiWriterCubit, + ), + BlocProvider( + create: (_) => AIPromptInputBloc( + predefinedFormat: null, + ), + ), + ], child: LayoutBuilder( builder: (context, constraints) { - 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, + return BlocListener( + listener: (context, state) { + if (state is SingleShotAiWriterState) { + showConfirmDialog( + context: context, + title: state.title, + description: state.description, + onConfirm: state.onDismiss, + ); + } + }, + child: OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return Stack( + children: [ + BlocBuilder( + builder: (context, state) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onTapOutside(), + onTapDown: (_) => onTapOutside(), + ); + }, ), - width: constraints.maxWidth, - child: Focus( - focusNode: focusNode, - child: OverlayContent( - editorState: editorState, - node: widget.node, - textController: textController, + CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + child: Container( + padding: const EdgeInsets.only( + left: 40.0, + bottom: 16.0, + ), + width: constraints.maxWidth, + child: OverlayContent( + node: widget.node, + ), ), ), - ), + ], + ); + }, + child: CompositedTransformTarget( + link: layerLink, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + key: key, + width: double.infinity, + ); + }, ), - ); - }, - child: CompositedTransformTarget( - link: layerLink, - child: BlocBuilder( - builder: (context, state) { - return SizedBox( - width: double.infinity, - height: 1.0, - ); - }, ), ), ); @@ -175,84 +204,78 @@ 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 StatefulWidget { +class OverlayContent extends StatelessWidget { 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) { - 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 selection = node.aiWriterSelection; + final showSuggestionPopup = + state is ReadyAiWriterState && !state.isFirstRun; + final showActionPopup = state is ReadyAiWriterState && state.isFirstRun; final markdownText = switch (state) { final ReadyAiWriterState ready => ready.markdownText, final GeneratingAiWriterState generating => generating.markdownText, _ => '', }; + final hasSelection = selection != null && !selection.isCollapsed; - final 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); + final isLightMode = Theme.of(context).isLightMode; + final darkBorderColor = + isLightMode ? Color(0x1F1F2329) : Color(0xFF505469); + final lightBorderColor = + Theme.of(context).brightness == Brightness.light + ? ColorSchemeConstants.lightBorderColor + : ColorSchemeConstants.darkBorderColor; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showSuggestedActionsPopup) ...[ + if (showSuggestionPopup && + state.command != AiWriterCommand.explain) ...[ Container( padding: EdgeInsets.all(4.0), decoration: _getModalDecoration( context, color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderColor: borderColor, + borderColor: darkBorderColor, ), child: SuggestionActionBar( - currentCommand: command, - hasSelection: hasSelection, + actions: _getSuggestedActions( + currentCommand: state.command, + hasSelection: hasSelection, + ), onTap: (action) { _onSelectSuggestionAction(context, action); }, @@ -260,81 +283,149 @@ class _OverlayContentState extends State { ), const VSpace(4.0 + 1.0), ], - Container( + DecoratedBox( decoration: _getModalDecoration( context, color: null, - borderColor: borderColor, + borderColor: darkBorderColor, borderRadius: BorderRadius.all(Radius.circular(12.0)), ), - constraints: BoxConstraints(maxHeight: 400), child: Column( - mainAxisSize: MainAxisSize.min, children: [ if (markdownText.isNotEmpty) ...[ - Flexible( - child: DecoratedBox( - decoration: _secondaryContentDecoration(context), - child: SecondaryContentArea( - markdownText: markdownText, - onSelectSuggestionAction: (action) { - _onSelectSuggestionAction(context, action); - }, - command: command, - showSuggestionActions: showSuggestedActionsWithin, - hasSelection: hasSelection, + 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), + ], ), ), ), - Divider(height: 1.0), + Divider( + height: 1.0, + ), ], DecoratedBox( decoration: markdownText.isNotEmpty - ? _mainContentDecoration(context) + ? _getInputChildDecoration(context) : _getSingleChildDeocoration(context), - child: MainContentArea( - textController: widget.textController, - isDocumentEmpty: _isDocumentEmpty(), - isInitialReadyState: isInitialReadyState, - showCommandsToggle: showCommandsToggle, - ), + child: MainContentArea(), ), ], ), ), - 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, - ); - }, + if (showActionPopup) ...[ + const VSpace(4.0 + 1.0), + Container( + padding: EdgeInsets.all(8.0), + constraints: BoxConstraints(minWidth: 240.0), + decoration: _getModalDecoration( + context, + color: Theme.of(context).colorScheme.surface, + borderColor: lightBorderColor, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + child: IntrinsicWidth( + child: SeparatedColumn( + separatorBuilder: () => const VSpace(4.0), + crossAxisAlignment: CrossAxisAlignment.start, + children: _getCommands( + hasSelection: hasSelection, + ), ), - ); - }, - ), + ), + ), + ], ], ); }, ); } + Widget _bottomButton(AiWriterCommand command) { + return Builder( + builder: (context) { + return SizedBox( + height: 30.0, + child: FlowyButton( + leftIcon: FlowySvg( + command.icon, + size: const Size.square(16), + color: Theme.of(context).iconTheme.color, + ), + margin: const EdgeInsets.all(6.0), + text: FlowyText( + command.i18n, + figmaLineHeight: 20, + ), + onTap: () { + final aiInputBloc = context.read(); + final showPredefinedFormats = + aiInputBloc.state.showPredefinedFormats; + final predefinedFormat = aiInputBloc.state.predefinedFormat; + + context.read().runCommand( + command, + showPredefinedFormats ? predefinedFormat : null, + ); + }, + ), + ); + }, + ); + } + BoxDecoration _getModalDecoration( BuildContext context, { required Color? color, @@ -348,9 +439,13 @@ class _OverlayContentState extends State { strokeAlign: BorderSide.strokeAlignOutside, ), borderRadius: borderRadius, - boxShadow: Theme.of(context).isLightMode - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall, + boxShadow: const [ + BoxShadow( + offset: Offset(0, 4), + blurRadius: 20, + color: Color(0x1A1F2329), + ), + ], ); } @@ -361,20 +456,85 @@ class _OverlayContentState extends State { ); } - BoxDecoration _secondaryContentDecoration(BuildContext context) { + BoxDecoration _getHelperChildDecoration(BuildContext context) { return BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)), ); } - BoxDecoration _mainContentDecoration(BuildContext context) { + BoxDecoration _getInputChildDecoration(BuildContext context) { return BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.vertical(bottom: Radius.circular(12.0)), ); } + List _getCommands({required bool hasSelection}) { + if (hasSelection) { + return [ + _bottomButton(AiWriterCommand.improveWriting), + _bottomButton(AiWriterCommand.fixSpellingAndGrammar), + _bottomButton(AiWriterCommand.explain), + const Divider(height: 1.0, thickness: 1.0), + _bottomButton(AiWriterCommand.makeLonger), + _bottomButton(AiWriterCommand.makeShorter), + ]; + } else { + return [ + _bottomButton(AiWriterCommand.continueWriting), + ]; + } + } + + List _getSuggestedActions({ + required AiWriterCommand currentCommand, + required bool hasSelection, + }) { + if (hasSelection) { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + AiWriterCommand.fixSpellingAndGrammar || + AiWriterCommand.improveWriting || + AiWriterCommand.makeShorter || + AiWriterCommand.makeLonger => + [ + SuggestionAction.accept, + SuggestionAction.discard, + SuggestionAction.insertBelow, + SuggestionAction.rewrite, + ], + }; + } else { + return switch (currentCommand) { + AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + AiWriterCommand.explain => [ + SuggestionAction.insertBelow, + SuggestionAction.tryAgain, + SuggestionAction.close, + ], + _ => [ + SuggestionAction.keep, + SuggestionAction.discard, + SuggestionAction.rewrite, + ], + }; + } + } + void _onSelectSuggestionAction( BuildContext context, SuggestionAction action, @@ -386,99 +546,10 @@ class _OverlayContentState extends State { predefinedFormat, ); } - - bool _isDocumentEmpty() { - if (widget.editorState.isEmptyForContinueWriting()) { - final documentContext = widget.editorState.document.root.context; - if (documentContext == null) { - return true; - } - final view = documentContext.read().state.view; - if (view.name.isEmpty) { - return true; - } - } - return false; - } -} - -class SecondaryContentArea extends StatelessWidget { - const SecondaryContentArea({ - super.key, - required this.command, - required this.markdownText, - required this.showSuggestionActions, - required this.hasSelection, - required this.onSelectSuggestionAction, - }); - - final AiWriterCommand command; - final String markdownText; - final bool showSuggestionActions; - final bool hasSelection; - final void Function(SuggestionAction) onSelectSuggestionAction; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(8.0), - Container( - height: 24.0, - padding: EdgeInsets.symmetric(horizontal: 14.0), - alignment: AlignmentDirectional.centerStart, - child: FlowyText( - command.i18n, - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF666D76), - ), - ), - const VSpace(4.0), - Flexible( - child: SingleChildScrollView( - physics: ClampingScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 14.0), - child: AIMarkdownText( - markdown: markdownText, - ), - ), - ), - if (showSuggestionActions) ...[ - const VSpace(4.0), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: SuggestionActionBar( - currentCommand: command, - hasSelection: hasSelection, - onTap: onSelectSuggestionAction, - ), - ), - ], - const VSpace(8.0), - ], - ), - ); - } } class MainContentArea extends StatelessWidget { - const MainContentArea({ - super.key, - required this.textController, - required this.isInitialReadyState, - required this.isDocumentEmpty, - required this.showCommandsToggle, - }); - - final TextEditingController textController; - final bool isInitialReadyState; - final bool isDocumentEmpty; - final ValueNotifier showCommandsToggle; + const MainContentArea({super.key}); @override Widget build(BuildContext context) { @@ -490,16 +561,7 @@ class MainContentArea extends StatelessWidget { return DesktopPromptInput( isStreaming: false, hideDecoration: true, - hideFormats: [ - AiWriterCommand.fixSpellingAndGrammar, - AiWriterCommand.improveWriting, - AiWriterCommand.makeLonger, - AiWriterCommand.makeShorter, - ].contains(state.command), - textController: textController, - onSubmitted: (message, format, _) { - cubit.runCommand(state.command, message, format); - }, + onSubmitted: (message, format, _) => cubit.submit(message, format), onStopStreaming: () => cubit.stopStream(), selectedSourcesNotifier: cubit.selectedSourcesNotifier, onUpdateSelectedSources: (sources) { @@ -507,18 +569,6 @@ class MainContentArea extends StatelessWidget { ...sources, ]; }, - extraBottomActionButton: isInitialReadyState - ? ValueListenableBuilder( - valueListenable: showCommandsToggle, - builder: (context, value, _) { - return AiWriterPromptMoreButton( - isEnabled: !isDocumentEmpty, - isSelected: value, - onTap: () => showCommandsToggle.value = !value, - ); - }, - ) - : null, ); } if (state is GeneratingAiWriterState) { @@ -574,26 +624,6 @@ class MainContentArea extends StatelessWidget { ), ); } - if (state is LocalAIStreamingAiWriterState) { - final text = switch (state.state) { - LocalAIStreamingState.notReady => - LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater.tr(), - LocalAIStreamingState.disabled => - LocaleKeys.settings_aiPage_keys_localAIDisabled.tr(), - }; - return Padding( - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - const HSpace(8.0), - Opacity( - opacity: 0.5, - child: FlowyText(text), - ), - ], - ), - ); - } return const SizedBox.shrink(); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart index 70d627d327..f7aa398046 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart @@ -1,13 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'operations/ai_writer_entities.dart'; @@ -17,7 +17,7 @@ const _aiWriterToolbarItemId = 'appflowy.editor.ai_writer'; final ToolbarItem improveWritingItem = ToolbarItem( id: _improveWritingToolbarItemId, group: 0, - isActive: onlyShowInTextTypeAndExcludeTable, + isActive: onlyShowInSingleSelectionAndTextType, builder: (context, editorState, _, __, tooltipBuilder) => ImproveWritingButton( editorState: editorState, @@ -28,7 +28,7 @@ final ToolbarItem improveWritingItem = ToolbarItem( final ToolbarItem aiWriterItem = ToolbarItem( id: _aiWriterToolbarItemId, group: 0, - isActive: onlyShowInTextTypeAndExcludeTable, + isActive: onlyShowInSingleSelectionAndTextType, builder: (context, editorState, _, __, tooltipBuilder) => AiWriterToolbarActionList( editorState: editorState, @@ -53,23 +53,18 @@ class AiWriterToolbarActionList extends StatefulWidget { class _AiWriterToolbarActionListState extends State { final popoverController = PopoverController(); - bool isSelected = false; @override Widget build(BuildContext context) { return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), + offset: const Offset(-8.0, 2.0), + margin: const EdgeInsets.all(8.0), onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, + onClose: () => keepEditorFocusNotifier.decrease(), popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), + child: buildChild(), ); } @@ -92,11 +87,12 @@ class _AiWriterToolbarActionListState extends State { Widget actionWrapper(AiWriterCommand command) { return SizedBox( - height: 36, + height: 30.0, child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.icon), - iconPadding: 12, + leftIcon: FlowySvg( + command.icon, + size: const Size.square(16), + ), text: FlowyText( command.i18n, figmaLineHeight: 20, @@ -116,38 +112,36 @@ class _AiWriterToolbarActionListState extends State { ); } - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; + Widget buildChild() { final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), + hoverColor: Colors.transparent, + width: 36, + height: 24, icon: Row( - mainAxisSize: MainAxisSize.min, children: [ - FlowySvg( - FlowySvgs.toolbar_ai_writer_m, - size: Size.square(20), - color: iconScheme.primary, + const FlowySvg( + FlowySvgs.ai_sparks_s, + size: Size.square(16.0), + color: Color(0xFFD08EED), ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconScheme.primary, + const FlowySvg( + FlowySvgs.ai_source_drop_down_s, + size: Size.square(12), + color: Color(0xFF8F959E), ), ], ), + iconPadding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), onPressed: () { - if (_isAIWriterEnabled(widget.editorState)) { + if (_isAIEnabled(widget.editorState)) { keepEditorFocusNotifier.increase(); popoverController.show(); - setState(() { - isSelected = true; - }); } else { showToastNotification( + context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } @@ -157,7 +151,7 @@ class _AiWriterToolbarActionListState extends State { return widget.tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIWriterEnabled(widget.editorState) + _isAIEnabled(widget.editorState) ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -178,22 +172,25 @@ class ImproveWritingButton extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_ai_improve_writing_m, - size: Size.square(20.0), - color: theme.iconColorScheme.primary, + hoverColor: Colors.transparent, + width: 24, + icon: const FlowySvg( + FlowySvgs.ai_improve_writing_s, + size: Size.square(16.0), + color: Color(0xFFD08EED), + ), + iconPadding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, ), onPressed: () { - if (_isAIWriterEnabled(editorState)) { + if (_isAIEnabled(editorState)) { keepEditorFocusNotifier.increase(); _insertAiNode(editorState, AiWriterCommand.improveWriting); } else { showToastNotification( + context, message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), ); } @@ -203,7 +200,7 @@ class ImproveWritingButton extends StatelessWidget { return tooltipBuilder?.call( context, _aiWriterToolbarItemId, - _isAIWriterEnabled(editorState) + _isAIEnabled(editorState) ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr() : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), child, @@ -238,8 +235,10 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async { ); } -bool _isAIWriterEnabled(EditorState editorState) { - return true; +bool _isAIEnabled(EditorState editorState) { + final documentContext = editorState.document.root.context; + return documentContext == null || + !documentContext.read().isLocalMode; } bool onlyShowInTextTypeAndExcludeTable( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart index 1b495a5b23..c82b39e241 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart @@ -1,45 +1,10 @@ -import 'dart:async'; - import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import '../ai_writer_block_component.dart'; import 'ai_writer_entities.dart'; -import 'ai_writer_node_extension.dart'; -Future setAiWriterNodeIsInitialized( - EditorState editorState, - Node node, -) async { - final transaction = editorState.transaction - ..updateNode(node, { - AiWriterBlockKeys.isInitialized: true, - }); - - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - inMemoryUpdate: true, - ), - withUpdateSelection: false, - ); - - final selection = node.aiWriterSelection; - if (selection != null && !selection.isCollapsed) { - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: {selectionExtraInfoDisableToolbar: true}, - ), - ); - } -} - -Future removeAiWriterNode( - EditorState editorState, - Node node, -) async { +Future removeAiWriterNode(EditorState editorState, Node node) async { final transaction = editorState.transaction..deleteNode(node); await editorState.apply( transaction, @@ -48,16 +13,16 @@ Future removeAiWriterNode( ); } -Future formatSelection( +void formatSelection( EditorState editorState, Selection selection, + Transaction transaction, ApplySuggestionFormatType formatType, -) async { +) { final nodes = editorState.getNodesInSelection(selection).toList(); if (nodes.isEmpty) { return; } - final transaction = editorState.transaction; if (nodes.length == 1) { final node = nodes.removeAt(0); @@ -103,43 +68,29 @@ Future formatSelection( } transaction.compose(); - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, - ); } -Future ensurePreviousNodeIsEmptyParagraph( +Position ensurePreviousNodeIsEmptyParagraph( EditorState editorState, Node aiWriterNode, -) async { + Transaction transaction, +) { final previous = aiWriterNode.previous; final needsEmptyParagraphNode = previous == null || previous.type != ParagraphBlockKeys.type || (previous.delta?.toPlainText().isNotEmpty ?? false); final Position position; - final transaction = editorState.transaction; - if (needsEmptyParagraphNode) { position = Position(path: aiWriterNode.path); transaction.insertNode(aiWriterNode.path, paragraphNode()); } else { position = Position(path: previous.path); } - transaction.afterSelection = Selection.collapsed(position); - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - ); + transaction.updateNode(aiWriterNode, { + AiWriterBlockKeys.isInitialized: true, + }); return position; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart index 4bc13321b8..56cdacdd6a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'package:appflowy/ai/ai.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import '../../base/markdown_text_robot.dart'; @@ -16,275 +16,260 @@ import 'ai_writer_block_operations.dart'; import 'ai_writer_entities.dart'; import 'ai_writer_node_extension.dart'; -/// Enable the debug log for the AiWriterCubit. -/// -/// This is useful for debugging the AI writer cubit. -const _aiWriterCubitDebugLog = true; - class AiWriterCubit extends Cubit { AiWriterCubit({ required this.documentId, required this.editorState, - this.onCreateNode, - this.onRemoveNode, - this.onAppendToDocument, + required this.getAiWriterNode, + required this.initialCommand, AppFlowyAIService? aiService, }) : _aiService = aiService ?? AppFlowyAIService(), _textRobot = MarkdownTextRobot(editorState: editorState), selectedSourcesNotifier = ValueNotifier([documentId]), - super(IdleAiWriterState()); + super( + ReadyAiWriterState( + initialCommand, + isFirstRun: true, + ), + ) { + editorState.service.keyboardService?.disableShortcuts(); + } final String documentId; final EditorState editorState; + final Node Function() getAiWriterNode; + final AiWriterCommand initialCommand; final AppFlowyAIService _aiService; final MarkdownTextRobot _textRobot; - final void Function()? onCreateNode; - final void Function()? onRemoveNode; - final void Function()? onAppendToDocument; - Node? aiWriterNode; - - final List records = []; final ValueNotifier> selectedSourcesNotifier; + (String, PredefinedFormat?)? _previousPrompt; + bool acceptReplacesOriginal = false; @override Future close() async { selectedSourcesNotifier.dispose(); + editorState.service.keyboardService?.enableShortcuts(); await super.close(); } - Future exit({ - bool withDiscard = true, - bool withUnformat = true, - }) async { - if (aiWriterNode == null) { - return; - } - if (withDiscard) { - await _textRobot.discard( - afterSelection: aiWriterNode!.aiWriterSelection, - ); - } - _textRobot.clear(); - _textRobot.reset(); - onRemoveNode?.call(); - records.clear(); - selectedSourcesNotifier.value = [documentId]; - emit(IdleAiWriterState()); - - if (withUnformat) { - final selection = aiWriterNode!.aiWriterSelection; - if (selection == null) { - return; - } - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, - ); - } - if (aiWriterNode != null) { - await removeAiWriterNode(editorState, aiWriterNode!); - aiWriterNode = null; - } + void init() { + runCommand(initialCommand, null, isImmediateRun: true); } - void register(Node node) async { - if (node.isAiWriterInitialized) { - return; - } - if (aiWriterNode != null && node.id != aiWriterNode!.id) { - await removeAiWriterNode(editorState, node); - return; - } + void submit( + String prompt, + PredefinedFormat? format, + ) async { + final command = AiWriterCommand.userQuestion; + final node = getAiWriterNode(); + _previousPrompt = (prompt, format); - aiWriterNode = node; - onCreateNode?.call(); - - await setAiWriterNodeIsInitialized(editorState, node); - - final command = node.aiWriterCommand; - final (run, prompt) = await _addSelectionTextToRecords(command); - - _aiWriterCubitLog( - 'command: $command, run: $run, prompt: $prompt', + final stream = await _aiService.streamCompletion( + objectId: documentId, + text: prompt, + format: format, + sourceIds: selectedSourcesNotifier.value, + completionType: command.toCompletionType(), + onStart: () async { + final transaction = editorState.transaction; + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + ); + _textRobot.start(position: position); + }, + onProcess: (text) async { + await _textRobot.appendMarkdownText( + text, + attributes: ApplySuggestionFormatType.replace.attributes, + ); + }, + onEnd: () async { + await _textRobot.stop( + attributes: ApplySuggestionFormatType.replace.attributes, + ); + emit(ReadyAiWriterState(command, isFirstRun: false)); + }, + onError: (error) async { + emit(ErrorAiWriterState(state.command, error: error)); + }, ); - if (!run) { - await exit(); - return; + if (stream != null) { + emit( + GeneratingAiWriterState( + command, + taskId: stream.$1, + ), + ); } - - runCommand(command, prompt, null); } void runCommand( AiWriterCommand command, - String prompt, - PredefinedFormat? predefinedFormat, - ) async { - if (aiWriterNode == null) { - return; - } - - await _textRobot.discard(); - _textRobot.clear(); - + PredefinedFormat? predefinedFormat, { + bool isImmediateRun = false, + bool isRetry = false, + }) async { switch (command) { case AiWriterCommand.continueWriting: await _startContinueWriting( command, predefinedFormat, + isImmediateRun: isImmediateRun, ); break; case AiWriterCommand.fixSpellingAndGrammar: case AiWriterCommand.improveWriting: case AiWriterCommand.makeLonger: case AiWriterCommand.makeShorter: - await _startSuggestingEdits(command, prompt, predefinedFormat); + await _startSuggestingEdits(command, predefinedFormat); break; case AiWriterCommand.explain: - await _startInforming(command, prompt, predefinedFormat); - break; - case AiWriterCommand.userQuestion when prompt.isNotEmpty: - _startAskingQuestion(prompt, predefinedFormat); + await _startInforming(command, predefinedFormat); break; case AiWriterCommand.userQuestion: - emit( - ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true), - ); + if (isRetry && _previousPrompt != null) { + submit(_previousPrompt!.$1, _previousPrompt!.$2); + } break; } } - void _retry({ - required PredefinedFormat? predefinedFormat, - }) async { - final lastQuestion = - records.lastWhereOrNull((record) => record.role == AiRole.user); - - if (lastQuestion != null && state is RegisteredAiWriter) { - runCommand( - (state as RegisteredAiWriter).command, - lastQuestion.content, - lastQuestion.format, - ); - } - } - - Future stopStream() async { - if (aiWriterNode == null) { + void stopStream() async { + if (state is! GeneratingAiWriterState) { return; } + final generatingState = state as GeneratingAiWriterState; + await AIEventStopCompleteText( + CompleteTextTaskPB( + taskId: generatingState.taskId, + ), + ).send(); + emit( + ReadyAiWriterState( + state.command, + isFirstRun: false, + markdownText: generatingState.markdownText, + ), + ); + } - if (state is GeneratingAiWriterState) { - final generatingState = state as GeneratingAiWriterState; - - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - - if (_textRobot.hasAnyResult) { - records.add(AiWriterRecord.ai(content: _textRobot.markdownText)); - } - - await AIEventStopCompleteText( - CompleteTextTaskPB( - taskId: generatingState.taskId, - ), - ).send(); - - emit( - ReadyAiWriterState( - generatingState.command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); + void exit() async { + await _textRobot.discard(); + final selection = getAiWriterNode().aiWriterSelection; + if (selection == null) { + return; } + final transaction = editorState.transaction; + formatSelection( + editorState, + selection, + transaction, + ApplySuggestionFormatType.clear, + ); + await editorState.apply( + transaction, + options: const ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), + withUpdateSelection: false, + ); + await removeAiWriterNode(editorState, getAiWriterNode()); } void runResponseAction( SuggestionAction action, [ PredefinedFormat? predefinedFormat, ]) async { - if (aiWriterNode == null) { - return; - } - if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { - _retry(predefinedFormat: predefinedFormat); - return; - } - if (action case SuggestionAction.discard || SuggestionAction.close) { - await exit(); + await _textRobot.discard(); + _textRobot.reset(); + runCommand(state.command, predefinedFormat, isRetry: true); return; } - final selection = aiWriterNode?.aiWriterSelection; + final selection = getAiWriterNode().aiWriterSelection; if (selection == null) { return; } - // Accept - // - // If the user clicks accept, we need to replace the selection with the AI's response - if (action case SuggestionAction.accept) { - // trim the markdown text to avoid extra new lines - final trimmedMarkdownText = _textRobot.markdownText.trim(); + if (action case SuggestionAction.discard || SuggestionAction.close) { + await _textRobot.discard(); - _aiWriterCubitLog( - 'trigger accept action, markdown text: $trimmedMarkdownText', - ); - - await formatSelection( + final transaction = editorState.transaction; + formatSelection( editorState, selection, + transaction, ApplySuggestionFormatType.clear, ); - - await _textRobot.deleteAINodes(); - - await _textRobot.replace( - selection: selection, - markdownText: trimmedMarkdownText, + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), ); - - await exit(withDiscard: false, withUnformat: false); - - return; } - if (action case SuggestionAction.keep) { + if (action case SuggestionAction.accept || SuggestionAction.keep) { await _textRobot.persist(); - await exit(withDiscard: false); - return; + + if (acceptReplacesOriginal) { + final nodes = editorState.getNodesInSelection(selection); + final transaction = editorState.transaction..deleteNodes(nodes); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, + ); + } } if (action case SuggestionAction.insertBelow) { - if (state is! ReadyAiWriterState) { - return; - } - final command = (state as ReadyAiWriterState).command; - final markdownText = (state as ReadyAiWriterState).markdownText; - if (command == AiWriterCommand.explain && markdownText.isNotEmpty) { - final position = await ensurePreviousNodeIsEmptyParagraph( + if (state case final ReadyAiWriterState readyState + when readyState.markdownText.isNotEmpty) { + final transaction = editorState.transaction; + final position = ensurePreviousNodeIsEmptyParagraph( editorState, - aiWriterNode!, + getAiWriterNode(), + transaction, + ); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, + ), ); _textRobot.start(position: position); - await _textRobot.persist(markdownText: markdownText); - } else if (_textRobot.hasAnyResult) { + await _textRobot.persist(markdownText: readyState.markdownText); + } else { await _textRobot.persist(); } - await formatSelection( + final transaction = editorState.transaction; + formatSelection( editorState, selection, + transaction, ApplySuggestionFormatType.clear, ); - await exit(withDiscard: false); + await editorState.apply( + transaction, + options: const ApplyOptions(recordUndo: false), + withUpdateSelection: false, + ); } + + await removeAiWriterNode(editorState, getAiWriterNode()); } bool hasUnusedResponse() { @@ -299,228 +284,86 @@ class AiWriterCubit extends Cubit { }; } - Future<(bool, String)> _addSelectionTextToRecords( - AiWriterCommand command, - ) async { - final node = aiWriterNode; - - // check the node is registered - if (node == null) { - return (false, ''); - } - - // check the selection is valid - final selection = node.aiWriterSelection?.normalized; - if (selection == null) { - return (false, ''); - } - - // if the command is continue writing, we don't need to get the selection text - if (command == AiWriterCommand.continueWriting) { - return (true, ''); - } - - // if the selection is collapsed, we don't need to get the selection text - if (selection.isCollapsed) { - return (true, ''); - } - - final selectionText = await editorState.getMarkdownInSelection(selection); - - if (command == AiWriterCommand.userQuestion) { - records.add( - AiWriterRecord.user(content: selectionText, format: null), - ); - - return (true, ''); - } else { - return (true, selectionText); - } - } - - Future _getDocumentContentFromTopToPosition(Position position) async { - final beginningToCursorSelection = Selection( - start: Position(path: [0]), - end: position, - ).normalized; - - final documentText = - (await editorState.getMarkdownInSelection(beginningToCursorSelection)) - .trim(); - - final view = await ViewBackendService.getView(documentId).toNullable(); - final viewName = view?.name ?? ''; - - return "$viewName\n$documentText".trim(); - } - - void _startAskingQuestion( - String prompt, - PredefinedFormat? format, - ) async { - if (aiWriterNode == null) { - return; - } - final command = AiWriterCommand.userQuestion; - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: prompt, - format: format, - history: records, - sourceIds: selectedSourcesNotifier.value, - completionType: command.toCompletionType(), - onStart: () async { - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position); - records.add( - AiWriterRecord.user( - content: prompt, - format: format, - ), - ); - }, - processMessage: (text) async { - await _textRobot.appendMarkdownText( - text, - updateSelection: false, - attributes: ApplySuggestionFormatType.replace.attributes, - ); - onAppendToDocument?.call(); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - }, - onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - } - }, - onError: (error) async { - emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, - ); - - if (stream != null) { - emit( - GeneratingAiWriterState( - command, - taskId: stream.$1, - ), - ); - } - } - Future _startContinueWriting( AiWriterCommand command, - PredefinedFormat? predefinedFormat, - ) async { - final position = aiWriterNode?.aiWriterSelection?.start; - if (position == null) { + PredefinedFormat? predefinedFormat, { + required bool isImmediateRun, + }) async { + final node = getAiWriterNode(); + + final cursorPosition = getAiWriterNode().aiWriterSelection?.start; + if (cursorPosition == null) { return; } - final text = await _getDocumentContentFromTopToPosition(position); + final selection = Selection( + start: Position(path: [0]), + end: cursorPosition, + ).normalized; + String text = await editorState.getMarkdownInSelection(selection); if (text.isEmpty) { - final stateCopy = state; - emit(DocumentContentEmptyAiWriterState(command, onConfirm: exit)); - emit(stateCopy); - return; + if (state is! ReadyAiWriterState) { + return; + } + final view = await ViewBackendService.getView(documentId).toNullable(); + if (view == null || + view.name.isEmpty || + view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) { + final readyState = state as ReadyAiWriterState; + emit( + SingleShotAiWriterState( + command, + title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), + description: + LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(), + onDismiss: () { + if (isImmediateRun) { + removeAiWriterNode(editorState, node); + } + }, + ), + ); + emit(readyState); + return; + } else { + text += view.name; + } } final stream = await _aiService.streamCompletion( objectId: documentId, text: text, completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, - format: predefinedFormat, onStart: () async { - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position); - records.add( - AiWriterRecord.user( - content: text, - format: predefinedFormat, + final transaction = editorState.transaction; + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, ), ); + _textRobot.start(position: position); }, - processMessage: (text) async { + onProcess: (text) async { await _textRobot.appendMarkdownText( text, - updateSelection: false, attributes: ApplySuggestionFormatType.replace.attributes, ); - onAppendToDocument?.call(); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } }, onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { + if (state case GeneratingAiWriterState _) { await _textRobot.stop( attributes: ApplySuggestionFormatType.replace.attributes, ); - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); + emit(ReadyAiWriterState(command, isFirstRun: false)); } - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); }, onError: (error) async { emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); if (stream != null) { @@ -532,71 +375,48 @@ class AiWriterCubit extends Cubit { Future _startSuggestingEdits( AiWriterCommand command, - String prompt, PredefinedFormat? predefinedFormat, ) async { - final selection = aiWriterNode?.aiWriterSelection; + final node = getAiWriterNode(); + final selection = node.aiWriterSelection; if (selection == null) { return; } - if (prompt.isEmpty) { - prompt = records.removeAt(0).content; - } + + acceptReplacesOriginal = true; final stream = await _aiService.streamCompletion( objectId: documentId, - text: prompt, - format: predefinedFormat, + text: await editorState.getMarkdownInSelection(selection), completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, onStart: () async { - await formatSelection( + final transaction = editorState.transaction; + formatSelection( editorState, selection, + transaction, ApplySuggestionFormatType.original, ); - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position, previousSelection: selection); - records.add( - AiWriterRecord.user( - content: prompt, - format: predefinedFormat, + final position = + ensurePreviousNodeIsEmptyParagraph(editorState, node, transaction); + transaction.afterSelection = null; + await editorState.apply( + transaction, + options: ApplyOptions( + inMemoryUpdate: true, + recordUndo: false, ), ); + _textRobot.start(position: position); }, - processMessage: (text) async { + onProcess: (text) async { await _textRobot.appendMarkdownText( text, - updateSelection: false, attributes: ApplySuggestionFormatType.replace.attributes, ); - onAppendToDocument?.call(); - - _aiWriterCubitLog( - 'received message: $text', - ); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - - _aiWriterCubitLog( - 'received assist message: $text', - ); }, onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { + if (state is GeneratingAiWriterState) { await _textRobot.stop( attributes: ApplySuggestionFormatType.replace.attributes, ); @@ -604,26 +424,12 @@ class AiWriterCubit extends Cubit { ReadyAiWriterState( command, isFirstRun: false, - markdownText: generatingState.markdownText, ), ); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - - _aiWriterCubitLog( - 'returned response: ${_textRobot.markdownText}', - ); } }, onError: (error) async { emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); }, ); if (stream != null) { @@ -635,33 +441,20 @@ class AiWriterCubit extends Cubit { Future _startInforming( AiWriterCommand command, - String prompt, PredefinedFormat? predefinedFormat, ) async { - final selection = aiWriterNode?.aiWriterSelection; + final node = getAiWriterNode(); + final selection = node.aiWriterSelection; if (selection == null) { return; } - if (prompt.isEmpty) { - prompt = records.removeAt(0).content; - } final stream = await _aiService.streamCompletion( objectId: documentId, - text: prompt, + text: await editorState.getMarkdownInSelection(selection), completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, - format: predefinedFormat, - onStart: () async { - records.add( - AiWriterRecord.user( - content: prompt, - format: predefinedFormat, - ), - ); - }, - processMessage: (text) async { + onStart: () async {}, + onProcess: (text) async { if (state case final GeneratingAiWriterState generatingState) { emit( GeneratingAiWriterState( @@ -672,7 +465,6 @@ class AiWriterCubit extends Cubit { ); } }, - processAssistMessage: (_) async {}, onEnd: () async { if (state case final GeneratingAiWriterState generatingState) { emit( @@ -682,22 +474,11 @@ class AiWriterCubit extends Cubit { markdownText: generatingState.markdownText, ), ); - records.add( - AiWriterRecord.ai(content: generatingState.markdownText), - ); } }, onError: (error) async { - if (state case final GeneratingAiWriterState generatingState) { - records.add( - AiWriterRecord.ai(content: generatingState.markdownText), - ); - } emit(ErrorAiWriterState(command, error: error)); }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, ); if (stream != null) { emit( @@ -705,90 +486,56 @@ class AiWriterCubit extends Cubit { ); } } - - void _aiWriterCubitLog(String message) { - if (_aiWriterCubitDebugLog) { - Log.debug('[AiWriterCubit] $message'); - } - } -} - -mixin RegisteredAiWriter { - AiWriterCommand get command; } sealed class AiWriterState { - const AiWriterState(); + const AiWriterState(this.command); + + final AiWriterCommand command; } -class IdleAiWriterState extends AiWriterState { - const IdleAiWriterState(); -} - -class ReadyAiWriterState extends AiWriterState with RegisteredAiWriter { +class ReadyAiWriterState extends AiWriterState { const ReadyAiWriterState( - this.command, { + super.command, { required this.isFirstRun, this.markdownText = '', }); - @override - final AiWriterCommand command; - final bool isFirstRun; final String markdownText; } -class GeneratingAiWriterState extends AiWriterState with RegisteredAiWriter { +class GeneratingAiWriterState extends AiWriterState { const GeneratingAiWriterState( - this.command, { + super.command, { required this.taskId, this.progress = '', this.markdownText = '', }); - @override - final AiWriterCommand command; - final String taskId; final String progress; final String markdownText; } -class ErrorAiWriterState extends AiWriterState with RegisteredAiWriter { +class ErrorAiWriterState extends AiWriterState { const ErrorAiWriterState( - this.command, { + super.command, { required this.error, }); - @override - final AiWriterCommand command; - final AIError error; } -class DocumentContentEmptyAiWriterState extends AiWriterState - with RegisteredAiWriter { - const DocumentContentEmptyAiWriterState( - this.command, { - required this.onConfirm, +class SingleShotAiWriterState extends AiWriterState { + const SingleShotAiWriterState( + super.command, { + required this.title, + required this.description, + required this.onDismiss, }); - @override - final AiWriterCommand command; - - final void Function() onConfirm; -} - -class LocalAIStreamingAiWriterState extends AiWriterState - with RegisteredAiWriter { - const LocalAIStreamingAiWriterState( - this.command, { - required this.state, - }); - - @override - final AiWriterCommand command; - - final LocalAIStreamingState state; + final String title; + final String description; + final void Function() onDismiss; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart index f15c2e6d7f..e0cc944b3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart @@ -1,9 +1,7 @@ -import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '../ai_writer_block_component.dart'; @@ -92,7 +90,7 @@ enum AiWriterCommand { FlowySvgData get icon => switch (this) { userQuestion => FlowySvgs.ai_sparks_s, - explain => FlowySvgs.ai_explain_m, + explain => FlowySvgs.ai_explain_s, // summarize => FlowySvgs.ai_summarize_s, continueWriting || improveWriting => FlowySvgs.ai_improve_writing_s, fixSpellingAndGrammar => FlowySvgs.ai_fix_spelling_grammar_s, @@ -122,38 +120,3 @@ enum ApplySuggestionFormatType { Map get attributes => {AiWriterBlockKeys.suggestion: value}; } - -enum AiRole { - user, - system, - ai, -} - -class AiWriterRecord extends Equatable { - const AiWriterRecord.user({ - required this.content, - required this.format, - }) : role = AiRole.user; - - const AiWriterRecord.ai({ - required this.content, - }) : role = AiRole.ai, - format = null; - - final AiRole role; - final String content; - final PredefinedFormat? format; - - @override - List get props => [role, content, format]; - - CompletionRecordPB toPB() { - return CompletionRecordPB( - content: content, - role: switch (role) { - AiRole.user => ChatMessageTypePB.User, - AiRole.system || AiRole.ai => ChatMessageTypePB.System, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart index 881871b154..1335f51df5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart @@ -1,5 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -34,15 +32,7 @@ extension AiWriterNodeExtension on EditorState { // if the selected nodes are not entirely selected, slice the nodes final slicedNodes = []; - final List flattenNodes = getNodesInSelection(selection); - final List nodes = []; - - for (final node in flattenNodes) { - if (nodes.any((element) => element.isParentOf(node))) { - continue; - } - nodes.add(node); - } + final nodes = getNodesInSelection(selection); for (final node in nodes) { final delta = node.delta; @@ -65,30 +55,11 @@ extension AiWriterNodeExtension on EditorState { slicedNodes.add(copiedNode); } - for (final (i, node) in slicedNodes.indexed) { - final childNodesShouldBeDeleted = []; - for (final child in node.children) { - if (!child.path.inSelection(selection)) { - childNodesShouldBeDeleted.add(child); - } - } - for (final child in childNodesShouldBeDeleted) { - slicedNodes[i] = node.copyWith( - children: node.children.where((e) => e.id != child.id).toList(), - type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type, - ); - } - } - - // use \n\n as line break to improve the ai response - // using \n will cause the ai response treat the text as a single line final markdown = await customDocumentToMarkdown( Document.blank()..insert([0], slicedNodes), - lineBreak: '\n', ); - // trim the last \n if it exists - return markdown.trimRight(); + return markdown; } List getPlainTextInSelection(Selection? selection) { @@ -116,64 +87,4 @@ extension AiWriterNodeExtension on EditorState { return res; } - - /// Determines whether the document is empty up to the selection - /// - /// If empty and the title is also empty, the continue writing option will be disabled. - bool isEmptyForContinueWriting({ - Selection? selection, - }) { - if (selection != null && !selection.isCollapsed) { - return false; - } - - final effectiveSelection = Selection( - start: Position(path: [0]), - end: selection?.normalized.end ?? - this.selection?.normalized.end ?? - Position(path: getLastSelectable()?.$1.path ?? [0]), - ); - - // if the selected nodes are not entirely selected, slice the nodes - final slicedNodes = []; - final nodes = getNodesInSelection(effectiveSelection); - - for (final node in nodes) { - final delta = node.delta; - if (delta == null) { - continue; - } - - final slicedDelta = delta.slice( - node == nodes.first ? effectiveSelection.startIndex : 0, - node == nodes.last ? effectiveSelection.endIndex : delta.length, - ); - - final copiedNode = node.copyWith( - attributes: { - ...node.attributes, - blockComponentDelta: slicedDelta.toJson(), - }, - ); - - slicedNodes.add(copiedNode); - } - - // using less custom parsers to avoid futures - final markdown = documentToMarkdown( - Document.blank()..insert([0], slicedNodes), - customParsers: [ - const MathEquationNodeParser(), - const CalloutNodeParser(), - const ToggleListNodeParser(), - const CustomParagraphNodeParser(), - const SubPageNodeParser(), - const SimpleTableNodeParser(), - const LinkPreviewNodeParser(), - const FileBlockNodeParser(), - ], - ); - - return markdown.trim().isEmpty; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart new file mode 100644 index 0000000000..a16fc44641 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/suggestion_action_bar.dart @@ -0,0 +1,63 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'operations/ai_writer_entities.dart'; + +class SuggestionActionBar extends StatelessWidget { + const SuggestionActionBar({ + super.key, + required this.actions, + required this.onTap, + }); + + final List actions; + final void Function(SuggestionAction) onTap; + + @override + Widget build(BuildContext context) { + return SeparatedRow( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const HSpace(4.0), + children: actions + .map( + (action) => SuggestionActionButton( + action: action, + onTap: () => onTap(action), + ), + ) + .toList(), + ); + } +} + +class SuggestionActionButton extends StatelessWidget { + const SuggestionActionButton({ + super.key, + required this.action, + required this.onTap, + }); + + final SuggestionAction action; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: FlowyButton( + text: FlowyText( + action.i18n, + figmaLineHeight: 20, + ), + leftIcon: action.buildIcon(context), + iconPadding: 4.0, + margin: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 4.0, + ), + onTap: onTap, + useIntrinsicWidth: true, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart deleted file mode 100644 index 8a691acdfc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -class AiWriterGestureDetector extends StatelessWidget { - const AiWriterGestureDetector({ - super.key, - required this.behavior, - required this.onPointerEvent, - this.child, - }); - - final HitTestBehavior behavior; - final void Function() onPointerEvent; - final Widget? child; - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - behavior: behavior, - gestures: { - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (instance) => instance - ..onTapDown = ((_) => onPointerEvent()) - ..onSecondaryTapDown = ((_) => onPointerEvent()) - ..onTertiaryTapDown = ((_) => onPointerEvent()), - ), - ImmediateMultiDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - ImmediateMultiDragGestureRecognizer>( - () => ImmediateMultiDragGestureRecognizer(), - (instance) => instance.onStart = (offset) => null, - ), - }, - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart deleted file mode 100644 index 72b8d9560b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import '../operations/ai_writer_entities.dart'; - -class AiWriterPromptMoreButton extends StatelessWidget { - const AiWriterPromptMoreButton({ - super.key, - required this.isEnabled, - required this.isSelected, - required this.onTap, - }); - - final bool isEnabled; - final bool isSelected; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: !isEnabled, - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - isSelected: () => isSelected, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.ai_more.tr(), - fontSize: 12, - figmaLineHeight: 16, - color: isEnabled - ? Theme.of(context).hintColor - : Theme.of(context).disabledColor, - ), - const HSpace(2.0), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class MoreAiWriterCommands extends StatelessWidget { - const MoreAiWriterCommands({ - super.key, - required this.hasSelection, - required this.editorState, - required this.onSelectCommand, - }); - - final EditorState editorState; - final bool hasSelection; - final void Function(AiWriterCommand) onSelectCommand; - - @override - Widget build(BuildContext context) { - return Container( - // add one here to take into account the border of the main message box. - // It is configured to be on the outside to hide some graphical - // artifacts. - margin: EdgeInsets.only(top: 4.0 + 1.0), - padding: EdgeInsets.all(8.0), - constraints: BoxConstraints(minWidth: 240.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: Theme.of(context).brightness == Brightness.light - ? ColorSchemeConstants.lightBorderColor - : ColorSchemeConstants.darkBorderColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - boxShadow: Theme.of(context).isLightMode - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall, - ), - child: IntrinsicWidth( - child: Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: _getCommands( - hasSelection: hasSelection, - ), - ), - ), - ); - } - - List _getCommands({required bool hasSelection}) { - if (hasSelection) { - return [ - _bottomButton(AiWriterCommand.improveWriting), - _bottomButton(AiWriterCommand.fixSpellingAndGrammar), - _bottomButton(AiWriterCommand.explain), - const Divider(height: 1.0, thickness: 1.0), - _bottomButton(AiWriterCommand.makeLonger), - _bottomButton(AiWriterCommand.makeShorter), - ]; - } else { - return [ - _bottomButton(AiWriterCommand.continueWriting), - ]; - } - } - - Widget _bottomButton(AiWriterCommand command) { - return Builder( - builder: (context) { - return FlowyButton( - leftIcon: FlowySvg( - command.icon, - color: Theme.of(context).iconTheme.color, - ), - leftIconSize: const Size.square(20), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: FlowyText( - command.i18n, - figmaLineHeight: 20, - ), - onTap: () => onSelectCommand(command), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart deleted file mode 100644 index ef8ee81219..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/throttle.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../operations/ai_writer_cubit.dart'; -import 'ai_writer_gesture_detector.dart'; - -class AiWriterScrollWrapper extends StatefulWidget { - const AiWriterScrollWrapper({ - super.key, - required this.viewId, - required this.editorState, - required this.child, - }); - - final String viewId; - final EditorState editorState; - final Widget child; - - @override - State createState() => _AiWriterScrollWrapperState(); -} - -class _AiWriterScrollWrapperState extends State { - final overlayController = OverlayPortalController(); - late final throttler = Throttler(); - late final aiWriterCubit = AiWriterCubit( - documentId: widget.viewId, - editorState: widget.editorState, - onCreateNode: () { - aiWriterRegistered = true; - widget.editorState.service.keyboardService?.disableShortcuts(); - }, - onRemoveNode: () { - aiWriterRegistered = false; - widget.editorState.service.keyboardService?.enableShortcuts(); - widget.editorState.service.keyboardService?.enable(); - }, - onAppendToDocument: onAppendToDocument, - ); - - bool userHasScrolled = false; - bool aiWriterRegistered = false; - bool dialogShown = false; - - @override - void initState() { - super.initState(); - overlayController.show(); - } - - @override - void dispose() { - aiWriterCubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: aiWriterCubit, - child: NotificationListener( - onNotification: handleScrollNotification, - child: Focus( - autofocus: true, - onKeyEvent: handleKeyEvent, - child: MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) { - if (state is DocumentContentEmptyAiWriterState) { - showConfirmDialog( - context: context, - title: - LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: LocaleKeys - .ai_continueWritingEmptyDocumentDescription - .tr(), - onConfirm: state.onConfirm, - ); - } - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous is GeneratingAiWriterState && - current is ReadyAiWriterState, - listener: (context, state) { - widget.editorState.updateSelectionWithReason(null); - }, - ), - ], - child: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return BlocBuilder( - builder: (context, state) { - return AiWriterGestureDetector( - behavior: state is RegisteredAiWriter - ? HitTestBehavior.translucent - : HitTestBehavior.deferToChild, - onPointerEvent: () => onTapOutside(context), - ); - }, - ); - }, - child: widget.child, - ), - ), - ), - ), - ); - } - - bool handleScrollNotification(ScrollNotification notification) { - if (!aiWriterRegistered) { - return false; - } - - if (notification is UserScrollNotification) { - debounceResetUserHasScrolled(); - userHasScrolled = true; - throttler.cancel(); - } - - return false; - } - - void debounceResetUserHasScrolled() { - Debounce.debounce( - 'user_has_scrolled', - const Duration(seconds: 3), - () => userHasScrolled = false, - ); - } - - void onTapOutside(BuildContext context) { - final aiWriterCubit = context.read(); - - if (aiWriterCubit.hasUnusedResponse()) { - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: stopAndExit, - onCancel: () {}, - ); - } else { - stopAndExit(); - } - } - - KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - if (!aiWriterRegistered) { - return KeyEventResult.ignored; - } - if (dialogShown) { - return KeyEventResult.handled; - } - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - - switch (event.logicalKey) { - case LogicalKeyboardKey.escape: - if (aiWriterCubit.state case GeneratingAiWriterState _) { - aiWriterCubit.stopStream(); - } else if (aiWriterCubit.hasUnusedResponse()) { - dialogShown = true; - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: stopAndExit, - onCancel: () {}, - ).then((_) => dialogShown = false); - } else { - stopAndExit(); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.keyC - when HardwareKeyboard.instance.isControlPressed: - if (aiWriterCubit.state case GeneratingAiWriterState _) { - aiWriterCubit.stopStream(); - } - return KeyEventResult.handled; - default: - break; - } - - return KeyEventResult.ignored; - } - - void onAppendToDocument() { - if (!aiWriterRegistered || userHasScrolled) { - return; - } - - throttler.call(() { - if (aiWriterCubit.aiWriterNode != null) { - final path = aiWriterCubit.aiWriterNode!.path; - - if (path.isEmpty) { - return; - } - - if (path.previous.isNotEmpty) { - final node = widget.editorState.getNodeAtPath(path.previous); - if (node != null && node.delta != null && node.delta!.isNotEmpty) { - widget.editorState.updateSelectionWithReason( - Selection.collapsed( - Position(path: path, offset: node.delta!.length), - ), - ); - return; - } - } - - widget.editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); - } - }); - } - - void stopAndExit() { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart deleted file mode 100644 index d39ede2608..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../operations/ai_writer_entities.dart'; - -class SuggestionActionBar extends StatelessWidget { - const SuggestionActionBar({ - super.key, - required this.currentCommand, - required this.hasSelection, - required this.onTap, - }); - - final AiWriterCommand currentCommand; - final bool hasSelection; - final void Function(SuggestionAction) onTap; - - @override - Widget build(BuildContext context) { - return SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(4.0), - children: _getSuggestedActions() - .map( - (action) => SuggestionActionButton( - action: action, - onTap: () => onTap(action), - ), - ) - .toList(), - ); - } - - List _getSuggestedActions() { - if (hasSelection) { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - AiWriterCommand.fixSpellingAndGrammar || - AiWriterCommand.improveWriting || - AiWriterCommand.makeShorter || - AiWriterCommand.makeLonger => - [ - SuggestionAction.accept, - SuggestionAction.discard, - SuggestionAction.insertBelow, - SuggestionAction.rewrite, - ], - }; - } else { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - _ => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - }; - } - } -} - -class SuggestionActionButton extends StatelessWidget { - const SuggestionActionButton({ - super.key, - required this.action, - required this.onTap, - }); - - final SuggestionAction action; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: FlowyButton( - text: FlowyText( - action.i18n, - figmaLineHeight: 20, - ), - leftIcon: action.buildIcon(context), - iconPadding: 4.0, - margin: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 4.0, - ), - onTap: onTap, - useIntrinsicWidth: true, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index 090ecdce78..f96a06b21d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; @@ -7,7 +6,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class BuiltInPageWidget extends StatefulWidget { const BuiltInPageWidget({ @@ -79,9 +77,6 @@ class _BuiltInPageWidgetState extends State { } Widget _buildPage(BuildContext context, ViewPB view) { - final verticalPadding = - context.read()?.verticalPadding ?? - 0.0; return Focus( focusNode: focusNode, onFocusChange: (value) { @@ -90,7 +85,7 @@ class _BuiltInPageWidgetState extends State { } }, child: Padding( - padding: EdgeInsets.symmetric(vertical: verticalPadding), + padding: const EdgeInsets.symmetric(vertical: 16), child: widget.builder(view), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 93b45cf46a..e25710c137 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -208,7 +208,7 @@ class _MobileEmojiPickerButton extends StatelessWidget { MobileEmojiPickerScreen.iconSelectedType: emoji.type.name, MobileEmojiPickerScreen.uploadDocumentId: documentId, MobileEmojiPickerScreen.selectTabs: - tabs.map((e) => e.name).toList().join('-'), + tabs.map((e) => e.name).toList(), }, ).toString(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index 11aed036d2..af605972de 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -70,12 +70,13 @@ extension InsertDatabase on EditorState { node, selection.end.offset, 0, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), + r'$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + }, + }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart index 259777db94..e818e9437b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart @@ -1,10 +1,8 @@ import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; import 'package:synchronized/synchronized.dart'; const _enableDebug = false; @@ -30,13 +28,8 @@ class MarkdownTextRobot { /// Only for debug via [_enableDebug]. final List _debugMarkdownTexts = []; - /// Selection before the refresh. - Selection? _previousSelection; - bool get hasAnyResult => _markdownText.isNotEmpty; - String get markdownText => _markdownText; - Selection? getInsertedSelection() { final position = _insertPosition; if (position == null) { @@ -61,11 +54,9 @@ class MarkdownTextRobot { } void start({ - Selection? previousSelection, Position? position, }) { - _insertPosition = position ?? editorState.selection?.start; - _previousSelection = previousSelection ?? editorState.selection; + _insertPosition ??= position ?? editorState.selection?.start; if (_enableDebug) { Log.info( @@ -77,7 +68,6 @@ class MarkdownTextRobot { /// The text will be inserted into the document but only in memory Future appendMarkdownText( String text, { - bool updateSelection = true, Map? attributes, }) async { _markdownText += text; @@ -85,7 +75,6 @@ class MarkdownTextRobot { await _lock.synchronized(() async { await _refresh( inMemoryUpdate: true, - updateSelection: updateSelection, attributes: attributes, ); }); @@ -104,21 +93,19 @@ class MarkdownTextRobot { await _lock.synchronized(() async { await _refresh( inMemoryUpdate: true, + updateSelection: false, attributes: attributes, ); }); } /// Persist the text into the document - Future persist({ - String? markdownText, - }) async { + Future persist({String? markdownText}) async { if (markdownText != null) { _markdownText = markdownText; } - await _lock.synchronized(() async { - await _refresh(inMemoryUpdate: false, updateSelection: true); + await _refresh(inMemoryUpdate: false); }); if (_enableDebug) { @@ -127,38 +114,8 @@ class MarkdownTextRobot { } } - /// Replace the selected content with the AI's response - Future replace({ - required Selection selection, - required String markdownText, - }) async { - if (selection.isSingle) { - await _replaceInSameLine( - selection: selection, - markdownText: markdownText, - ); - } else { - await _replaceInMultiLines( - selection: selection, - markdownText: markdownText, - ); - } - } - - /// Delete the temporary inserted AI nodes - Future deleteAINodes() async { - final nodes = getInsertedNodes(); - final transaction = editorState.transaction..deleteNodes(nodes); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - ); - } - /// Discard the inserted content - Future discard({ - Selection? afterSelection, - }) async { + Future discard() async { final start = _insertPosition; if (start == null) { return; @@ -167,8 +124,6 @@ class MarkdownTextRobot { return; } - afterSelection ??= Selection.collapsed(start); - // fallback to the calculated position if the selection is null. final end = Position( path: start.path.nextNPath(_insertedNodes.length - 1), @@ -178,11 +133,11 @@ class MarkdownTextRobot { ); final transaction = editorState.transaction ..deleteNodes(deletedNodes) - ..afterSelection = afterSelection; + ..afterSelection = Selection.collapsed(start); await editorState.apply( transaction, - options: const ApplyOptions(recordUndo: false, inMemoryUpdate: true), + options: const ApplyOptions(recordUndo: false), ); if (_enableDebug) { @@ -190,18 +145,15 @@ class MarkdownTextRobot { } } - void clear() { + void reset() { _markdownText = ''; _insertedNodes = []; - } - - void reset() { _insertPosition = null; } Future _refresh({ required bool inMemoryUpdate, - bool updateSelection = false, + bool updateSelection = true, Map? attributes, }) async { final position = _insertPosition; @@ -217,40 +169,11 @@ class MarkdownTextRobot { tableWidth: 250.0, ).root.children; - // check if the first selected node before the refresh is a numbered list node - final previousSelection = _previousSelection; - final previousSelectedNode = previousSelection == null - ? null - : editorState.getNodeAtPath(previousSelection.start.path); - final firstNodeIsNumberedList = previousSelectedNode != null && - previousSelectedNode.type == NumberedListBlockKeys.type; - final newNodes = attributes == null ? documentNodes - : documentNodes.mapIndexed((index, node) { - final n = _styleDelta(node: node, attributes: attributes); - n.externalValues = AINodeExternalValues( - isAINode: true, - ); - if (index == 0 && n.type == NumberedListBlockKeys.type) { - if (firstNodeIsNumberedList) { - final builder = NumberedListIndexBuilder( - editorState: editorState, - node: previousSelectedNode, - ); - final firstIndex = builder.indexInSameLevel; - n.updateAttributes({ - NumberedListBlockKeys.number: firstIndex, - }); - } - - n.externalValues = AINodeExternalValues( - isAINode: true, - isFirstNumberedListNode: true, - ); - } - return n; - }).toList(); + : documentNodes + .map((node) => _styleDelta(node: node, attributes: attributes)) + .toList(); if (newNodes.isEmpty) { return; @@ -278,6 +201,10 @@ class MarkdownTextRobot { offset: lastDelta.length, ), ); + + if (!updateSelection) { + insertTransaction.afterSelection = null; + } } await editorState.apply( @@ -286,7 +213,6 @@ class MarkdownTextRobot { inMemoryUpdate: inMemoryUpdate, recordUndo: !inMemoryUpdate, ), - withUpdateSelection: updateSelection, ); _insertedNodes = newNodes; @@ -317,250 +243,4 @@ class MarkdownTextRobot { children: children, ); } - - /// If the selected content is in the same line, - /// keep the selected node and replace the delta. - Future _replaceInSameLine({ - required Selection selection, - required String markdownText, - }) async { - if (markdownText.isEmpty) { - assert(false, 'Expected non-empty markdown text'); - Log.error('Expected non-empty markdown text'); - return; - } - - selection = selection.normalized; - - // If the selection is not a single node, do nothing. - if (!selection.isSingle) { - assert(false, 'Expected single node selection'); - Log.error('Expected single node selection'); - return; - } - - final startIndex = selection.startIndex; - final endIndex = selection.endIndex; - final length = endIndex - startIndex; - - // Get the selected node. - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - assert(false, 'Expected non-null node and delta'); - Log.error('Expected non-null node and delta'); - return; - } - - // Convert the markdown text to delta. - // Question: Why we need to convert the markdown to document first? - // Answer: Because the markdown text may contain the list item, - // if we convert the markdown to delta directly, the list item will be - // treated as a normal text node, and the delta will be incorrect. - // For example, the markdown text is: - // ``` - // 1. item1 - // ``` - // if we convert the markdown to delta directly, the delta will be: - // ``` - // [ - // { - // "insert": "1. item1" - // } - // ] - // ``` - // if we convert the markdown to document first, the document will be: - // ``` - // [ - // { - // "type": "numbered_list", - // "children": [ - // { - // "insert": "item1" - // } - // ] - // } - // ] - final document = customMarkdownToDocument(markdownText); - final nodes = document.root.children; - final decoder = DeltaMarkdownDecoder(); - final markdownDelta = - nodes.firstOrNull?.delta ?? decoder.convert(markdownText); - - if (markdownDelta.isEmpty) { - assert(false, 'Expected non-empty markdown delta'); - Log.error('Expected non-empty markdown delta'); - return; - } - - // Replace the delta of the selected node. - final transaction = editorState.transaction; - - // it means the user selected the entire sentence, we just replace the node - if (startIndex == 0 && length == node.delta?.length) { - if (nodes.isNotEmpty && node.children.isNotEmpty) { - // merge the children of the selected node and the first node of the ai response - nodes[0] = nodes[0].copyWith( - children: [ - ...node.children.map((e) => e.deepCopy()), - ...nodes[0].children, - ], - ); - } - transaction - ..insertNodes(node.path.next, nodes) - ..deleteNode(node); - } else { - // it means the user selected a part of the sentence, we need to delete the - // selected part and insert the new delta. - transaction - ..deleteText(node, startIndex, length) - ..insertTextDelta(node, startIndex, markdownDelta); - - // Add the remaining nodes to the document. - final remainingNodes = nodes.skip(1); - if (remainingNodes.isNotEmpty) { - transaction.insertNodes( - node.path.next, - remainingNodes, - ); - } - } - - await editorState.apply(transaction); - } - - /// If the selected content is in multiple lines - Future _replaceInMultiLines({ - required Selection selection, - required String markdownText, - }) async { - selection = selection.normalized; - - // If the selection is a single node, do nothing. - if (selection.isSingle) { - assert(false, 'Expected multi-line selection'); - Log.error('Expected multi-line selection'); - return; - } - - final markdownNodes = customMarkdownToDocument( - markdownText, - tableWidth: 250.0, - ).root.children; - - // Get the selected nodes. - final flattenNodes = editorState.getNodesInSelection(selection); - final nodes = []; - for (final node in flattenNodes) { - if (nodes.any((element) => element.isParentOf(node))) { - continue; - } - nodes.add(node); - } - - // Note: Don't change its order, otherwise the delta will be incorrect. - // step 1. merge the first selected node and the first node from the ai response - // step 2. merge the last selected node and the last node from the ai response - // step 3. insert the middle nodes from the ai response - // step 4. delete the middle nodes - final transaction = editorState.transaction; - - // step 1 - final firstNode = nodes.firstOrNull; - final delta = firstNode?.delta; - final firstMarkdownNode = markdownNodes.firstOrNull; - final firstMarkdownDelta = firstMarkdownNode?.delta; - if (firstNode != null && - delta != null && - firstMarkdownNode != null && - firstMarkdownDelta != null) { - final startIndex = selection.startIndex; - final length = delta.length - startIndex; - - transaction - ..deleteText(firstNode, startIndex, length) - ..insertTextDelta(firstNode, startIndex, firstMarkdownDelta); - - // if the first markdown node has children, we need to insert the children - // and delete the children of the first node that are in the selection. - if (firstMarkdownNode.children.isNotEmpty) { - transaction.insertNodes( - firstNode.path.child(0), - firstMarkdownNode.children.map((e) => e.deepCopy()), - ); - } - - final nodesToDelete = - firstNode.children.where((e) => e.path.inSelection(selection)); - transaction.deleteNodes(nodesToDelete); - } - - // step 2 - bool handledLastNode = false; - final lastNode = nodes.lastOrNull; - final lastDelta = lastNode?.delta; - final lastMarkdownNode = markdownNodes.lastOrNull; - final lastMarkdownDelta = lastMarkdownNode?.delta; - if (lastNode != null && - lastDelta != null && - lastMarkdownNode != null && - lastMarkdownDelta != null && - firstNode?.id != lastNode.id) { - handledLastNode = true; - - final endIndex = selection.endIndex; - - transaction.deleteText(lastNode, 0, endIndex); - - // if the last node is same as the first node, it means we have replaced the - // selected text in the first node. - if (lastMarkdownNode.id != firstMarkdownNode?.id) { - transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta); - - if (lastMarkdownNode.children.isNotEmpty) { - transaction - ..insertNodes( - lastNode.path.child(0), - lastMarkdownNode.children.map((e) => e.deepCopy()), - ) - ..deleteNodes( - lastNode.children.where((e) => e.path.inSelection(selection)), - ); - } - } - } - - // step 3 - final insertedPath = selection.start.path.nextNPath(1); - final insertLength = handledLastNode ? 2 : 1; - if (markdownNodes.length > insertLength) { - transaction.insertNodes( - insertedPath, - markdownNodes - .skip(1) - .take(markdownNodes.length - insertLength) - .toList(), - ); - } - - // step 4 - final length = nodes.length - 2; - if (length > 0) { - final middleNodes = nodes.skip(1).take(length).toList(); - transaction.deleteNodes(middleNodes); - } - - await editorState.apply(transaction); - } -} - -class AINodeExternalValues extends NodeExternalValues { - const AINodeExternalValues({ - this.isAINode = false, - this.isFirstNumberedListNode = false, - }); - - final bool isAINode; - final bool isFirstNumberedListNode; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart index 77245a9f95..aed78172b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart @@ -1,8 +1,4 @@ -import 'dart:ui'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; bool _isTableType(String type) { @@ -36,31 +32,3 @@ bool onlyShowInSingleTextTypeSelectionAndExcludeTable( return onlyShowInSingleSelectionAndTextType(editorState) && notShowInTable(editorState); } - -bool enableSuggestions(EditorState editorState) { - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return false; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return false; - } - if (isNarrowWindow(editorState)) return false; - - return (node.delta != null && suggestionsItemTypes.contains(node.type)) && - notShowInTable(editorState); -} - -bool isNarrowWindow(EditorState editorState) { - final editorSize = editorState.renderBox?.size ?? Size.zero; - if (editorSize.width < 650) return true; - return false; -} - -final Set suggestionsItemTypes = { - ...toolbarItemWhiteList, - ToggleListBlockKeys.type, - TodoListBlockKeys.type, - CalloutBlockKeys.type, -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index a7fcccd186..1d9f70254f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -1,8 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; @@ -81,7 +80,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { }); final Color defaultColor; - final EdgeInsets Function(Node node) inlinePadding; + final EdgeInsets inlinePadding; @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -97,15 +96,12 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (node) => node.delta != null; + BlockComponentValidate get validate => + (node) => node.delta != null && node.children.isEmpty; } // the main widget for rendering the callout block @@ -115,14 +111,13 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.defaultColor, required this.inlinePadding, }); final Color defaultColor; - final EdgeInsets Function(Node node) inlinePadding; + final EdgeInsets inlinePadding; @override State createState() => @@ -137,8 +132,7 @@ class _CalloutBlockComponentWidgetState BlockComponentConfigurable, BlockComponentTextDirectionMixin, BlockComponentAlignMixin, - BlockComponentBackgroundColorMixin, - NestedBlockComponentStatefulWidgetMixin { + BlockComponentBackgroundColorMixin { // the key used to forward focus to the richtext child @override final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); @@ -175,85 +169,41 @@ class _CalloutBlockComponentWidgetState EmojiIconData result = EmojiIconData.emoji('📌'); try { result = EmojiIconData(FlowyIconType.values.byName(type), icon); - } catch (_) {} + } catch (e) { + Log.error( + 'get emoji error with icon:[$icon], type:[$type] within alloutBlockComponentWidget', + e, + ); + } return result; } + // get access to the editor state via provider @override - Widget build(BuildContext context) { - Widget child = node.children.isEmpty - ? buildComponent(context) - : buildComponentWithChildren(context); - - if (UniversalPlatform.isDesktop) { - child = Padding( - padding: EdgeInsets.symmetric(vertical: 2.0), - child: child, - ); - } - - return child; - } - - @override - Widget buildComponentWithChildren(BuildContext context) { - Widget child = Stack( - children: [ - Positioned.fill( - left: UniversalPlatform.isMobile ? 0 : cachedLeft, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - color: backgroundColor, - ), - ), - ), - NestedListWidget( - indentPadding: indentPadding.copyWith(bottom: 8), - child: buildComponent(context, withBackgroundColor: false), - children: editorState.renderer.buildList( - context, - widget.node.children, - ), - ), - ], - ); - - if (UniversalPlatform.isMobile) { - child = Padding( - padding: padding, - child: child, - ); - } - - return child; - } + late final editorState = Provider.of(context, listen: false); // build the callout block widget @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = true, - }) { + Widget build(BuildContext context) { final textDirection = calculateTextDirection( layoutDirection: Directionality.maybeOf(context), ); final (emojiSize, emojiButtonSize) = calculateEmojiSize(); - final documentId = context.read()?.documentId; + Widget child = Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - color: withBackgroundColor ? backgroundColor : null, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: backgroundColor, ), - padding: widget.inlinePadding(widget.node), + padding: widget.inlinePadding, width: double.infinity, alignment: alignment, child: Row( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, textDirection: textDirection, children: [ - const HSpace(6.0), + if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0), // the emoji picker button for the note EmojiPickerButton( // force to refresh the popover state @@ -267,18 +217,12 @@ class _CalloutBlockComponentWidgetState emojiSize: emojiSize, showBorder: false, buttonSize: emojiButtonSize, - documentId: documentId, - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], onSubmitted: (r, controller) { setEmojiIconData(r.data); if (!r.keepOpen) controller?.close(); }, ), - if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), + if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0), Flexible( child: Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), @@ -290,26 +234,17 @@ class _CalloutBlockComponentWidgetState ), ); - if (UniversalPlatform.isMobile && node.children.isEmpty) { - child = Padding( - key: blockComponentKey, - padding: padding, - child: child, - ); - } else { - child = Container( - key: blockComponentKey, - padding: EdgeInsets.zero, - child: child, - ); - } + child = Padding( + key: blockComponentKey, + padding: padding, + child: child, + ); child = BlockSelectionContainer( node: node, delegate: this, listenable: editorState.selectionNotifier, blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, supportTypes: const [ BlockSelectionType.block, ], @@ -320,7 +255,6 @@ class _CalloutBlockComponentWidgetState child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart index 842f3f59fd..3c50661071 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart @@ -32,22 +32,9 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { await editorState.deleteSelection(selection); if (HardwareKeyboard.instance.isShiftPressed) { - // ignore the shift+enter event, fallback to the default behavior - return false; - } else if (node.children.isEmpty) { - // insert a new paragraph within the callout block - final path = node.path.child(0); - final transaction = editorState.transaction; - transaction.insertNode( - path, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - await editorState.apply(transaction); + await editorState.insertNewLine(); + } else { + await editorState.insertTextAtCurrentSelection('\n'); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart index 645de3b2f8..cc80119cb9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart @@ -47,6 +47,7 @@ class _CopyButton extends StatelessWidget { if (context.mounted) { showToastNotification( + context, message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart index c426ad640f..611f7ec67f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -7,62 +7,27 @@ import 'package:provider/provider.dart'; Node simpleColumnNode({ List? children, - double? ratio, + double? width, }) { return Node( type: SimpleColumnBlockKeys.type, children: children ?? [paragraphNode()], attributes: { - SimpleColumnBlockKeys.ratio: ratio, + SimpleColumnBlockKeys.width: width, }, ); } -extension SimpleColumnBlockAttributes on Node { - // get the next column node of the current column node - // if the current column node is the last column node, return null - Node? get nextColumn { - final index = path.last; - final parent = this.parent; - if (parent == null || index == parent.children.length - 1) { - return null; - } - return parent.children[index + 1]; - } - - // get the previous column node of the current column node - // if the current column node is the first column node, return null - Node? get previousColumn { - final index = path.last; - final parent = this.parent; - if (parent == null || index == 0) { - return null; - } - return parent.children[index - 1]; - } -} - class SimpleColumnBlockKeys { const SimpleColumnBlockKeys._(); static const String type = 'simple_column'; - /// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead. - /// - /// This field is no longer used since v0.6.9 - @Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.') static const String width = 'width'; - - /// The ratio of the column width. - /// - /// The value is a double number between 0 and 1. - static const String ratio = 'ratio'; } class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder { - SimpleColumnBlockComponentBuilder({ - super.configuration, - }); + SimpleColumnBlockComponentBuilder({super.configuration}); @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -86,7 +51,6 @@ class SimpleColumnBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -109,6 +73,16 @@ class SimpleColumnBlockComponentState extends State late final EditorState editorState = context.read(); + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + @override Widget build(BuildContext context) { Widget child = Column( @@ -116,13 +90,12 @@ class SimpleColumnBlockComponentState extends State crossAxisAlignment: CrossAxisAlignment.start, children: node.children.map( (e) { - Widget child = Provider( - create: (_) => DatabasePluginWidgetBuilderSize( - verticalPadding: 0, - horizontalPadding: 0, - ), + Widget child = IntrinsicHeight( child: editorState.renderer.build(context, e), ); + if (e.type == CustomImageBlockKeys.type) { + child = IntrinsicWidth(child: child); + } if (SimpleColumnsBlockConstants.enableDebugBorder) { child = DecoratedBox( decoration: BoxDecoration( @@ -147,7 +120,7 @@ class SimpleColumnBlockComponentState extends State if (SimpleColumnsBlockConstants.enableDebugBorder) { child = Container( color: Colors.green.withValues( - alpha: 0.3, + alpha: 0.2, ), child: child, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart index 69bec33c61..dad0dea132 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -9,12 +10,10 @@ class SimpleColumnBlockWidthResizer extends StatefulWidget { super.key, required this.columnNode, required this.editorState, - this.height, }); final Node columnNode; final EditorState editorState; - final double? height; @override State createState() => @@ -27,13 +26,6 @@ class _SimpleColumnBlockWidthResizerState ValueNotifier isHovering = ValueNotifier(false); - @override - void dispose() { - isHovering.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return MouseRegion( @@ -55,14 +47,15 @@ class _SimpleColumnBlockWidthResizerState child: ValueListenableBuilder( valueListenable: isHovering, builder: (context, isHovering, child) { - final hide = isDraggingAppFlowyEditorBlock.value || !isHovering; + if (isDraggingAppFlowyEditorBlock.value) { + return SizedBox.shrink(); + } return MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: Container( width: 2, - height: widget.height ?? 20, margin: EdgeInsets.symmetric(horizontal: 2), - color: !hide + color: isHovering ? Theme.of(context).colorScheme.primary : Colors.transparent, ), @@ -85,47 +78,25 @@ class _SimpleColumnBlockWidthResizerState // update the column width in memory final columnNode = widget.columnNode; - final columnsNode = columnNode.columnsParent; - if (columnsNode == null) { - return; - } - final editorWidth = columnsNode.rect.width; final rect = columnNode.rect; - final width = rect.width; - final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio]; + final width = + columnNode.attributes[SimpleColumnBlockKeys.width] ?? rect.width; final newWidth = width + details.delta.dx; - final transaction = widget.editorState.transaction; - final newRatio = newWidth / editorWidth; transaction.updateNode(columnNode, { ...columnNode.attributes, - SimpleColumnBlockKeys.ratio: newRatio, + SimpleColumnBlockKeys.width: newWidth.clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), }); - - if (newRatio < 0.1 && newRatio < originalRatio) { - return; - } - - final nextColumn = columnNode.nextColumn; - if (nextColumn != null) { - final nextColumnRect = nextColumn.rect; - final nextColumnWidth = nextColumnRect.width; - final newNextColumnWidth = nextColumnWidth - details.delta.dx; - final newNextColumnRatio = newNextColumnWidth / editorWidth; - if (newNextColumnRatio < 0.1) { - return; - } - transaction.updateNode(nextColumn, { - ...nextColumn.attributes, - SimpleColumnBlockKeys.ratio: newNextColumnRatio, + final columnsNode = columnNode.parent; + if (columnsNode != null) { + transaction.updateNode(columnsNode, { + ...columnsNode.attributes, + ColumnsBlockKeys.columnCount: columnsNode.children.length, }); } - - transaction.updateNode(columnsNode, { - ...columnsNode.attributes, - ColumnsBlockKeys.columnCount: columnsNode.children.length, - }); - widget.editorState.apply( transaction, options: ApplyOptions(inMemoryUpdate: true), @@ -142,15 +113,9 @@ class _SimpleColumnBlockWidthResizerState // apply the transaction again to make sure the width is updated final transaction = widget.editorState.transaction; - final columnsNode = widget.columnNode.columnsParent; - if (columnsNode == null) { - return; - } - for (final columnNode in columnsNode.children) { - transaction.updateNode(columnNode, { - ...columnNode.attributes, - }); - } + transaction.updateNode(widget.columnNode, { + ...widget.columnNode.attributes, + }); widget.editorState.apply(transaction); isDragging = false; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart index 05389fb760..3f4f73fd49 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart @@ -3,7 +3,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; extension SimpleColumnNodeExtension on Node { /// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock]. - Node? get columnsParent { + Node? get parentColumnsBlock { Node? currentNode = parent; while (currentNode != null) { if (currentNode.type == SimpleColumnsBlockKeys.type) { @@ -15,7 +15,7 @@ extension SimpleColumnNodeExtension on Node { } /// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock]. - Node? get columnParent { + Node? get parentColumn { Node? currentNode = parent; while (currentNode != null) { if (currentNode.type == SimpleColumnBlockKeys.type) { @@ -27,8 +27,8 @@ extension SimpleColumnNodeExtension on Node { } /// Returns whether the current node is in a [SimpleColumnsBlock]. - bool get isInColumnsBlock => columnsParent != null; + bool get isInColumnsBlock => parentColumnsBlock != null; /// Returns whether the current node is in a [SimpleColumnBlock]. - bool get isInColumnBlock => columnParent != null; + bool get isInColumnBlock => parentColumn != null; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart index 58ecde5f2f..43fd779d33 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; @@ -7,19 +5,20 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:universal_platform/universal_platform.dart'; // if the children is not provided, it will create two columns by default. // if the columnCount is provided, it will create the specified number of columns. Node simpleColumnsNode({ List? children, int? columnCount, - double? ratio, + double? width, }) { columnCount ??= 2; children ??= List.generate( columnCount, (index) => simpleColumnNode( - ratio: ratio, + width: width, children: [paragraphNode()], ), ); @@ -68,7 +67,6 @@ class ColumnsBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -92,32 +90,37 @@ class ColumnsBlockComponentState extends State final ScrollController scrollController = ScrollController(); - final ValueNotifier heightValueNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _updateColumnsBlock(); - } - @override void dispose() { scrollController.dispose(); - heightValueNotifier.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - Widget child = Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildChildren(), + Widget child = SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: scrollController, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildChildren(), + ), ); + if (UniversalPlatform.isDesktop) { + // only show the scrollbar on desktop + child = Scrollbar( + controller: scrollController, + child: child, + ); + } + child = Align( alignment: Alignment.topLeft, - child: child, + child: IntrinsicHeight( + child: child, + ), ); child = Padding( @@ -140,78 +143,38 @@ class ColumnsBlockComponentState extends State // the columns block does not support the block actions and selection // because the columns block is a layout wrapper, it does not have a content - return NotificationListener( - onNotification: (v) => updateHeightValueNotifier(v), - child: SizeChangedLayoutNotifier(child: child), - ); + return child; } List _buildChildren() { - final length = node.children.length; final children = []; - for (var i = 0; i < length; i++) { + for (var i = 0; i < node.children.length; i++) { final childNode = node.children[i]; - final double ratio = - childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length; - + final width = + childNode.attributes[SimpleColumnBlockKeys.width]?.toDouble() ?? + SimpleColumnsBlockConstants.minimumColumnWidth; Widget child = editorState.renderer.build(context, childNode); - child = Expanded( - flex: (max(ratio, 0.1) * 10000).toInt(), + child = SizedBox( + width: width.clamp( + SimpleColumnsBlockConstants.minimumColumnWidth, + double.infinity, + ), child: child, ); children.add(child); - if (i != length - 1) { - children.add( - ValueListenableBuilder( - valueListenable: heightValueNotifier, - builder: (context, height, child) { - return SimpleColumnBlockWidthResizer( - columnNode: childNode, - editorState: editorState, - height: height, - ); - }, - ), - ); - } + children.add( + SimpleColumnBlockWidthResizer( + columnNode: childNode, + editorState: editorState, + ), + ); } return children; } - // Update the existing columns block data - // if the column ratio is not existing, it will be set to 1.0 / columnCount - void _updateColumnsBlock() { - final transaction = editorState.transaction; - final length = node.children.length; - for (int i = 0; i < length; i++) { - final childNode = node.children[i]; - final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio]; - if (ratio == null) { - transaction.updateNode(childNode, { - ...childNode.attributes, - SimpleColumnBlockKeys.ratio: 1.0 / length, - }); - } - } - if (transaction.operations.isNotEmpty) { - editorState.apply(transaction); - } - } - - bool updateHeightValueNotifier(SizeChangedLayoutNotification notification) { - if (!mounted) return true; - final height = _renderBox?.size.height; - if (heightValueNotifier.value == height) return true; - WidgetsBinding.instance.addPostFrameCallback((_) { - heightValueNotifier.value = height; - }); - return true; - } - @override Position start() => Position(path: widget.node.path); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 6399d3b11f..5c25d470d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -16,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; /// - support /// - desktop @@ -163,7 +162,6 @@ Future _pasteAsLinkPreview( EditorState editorState, String? text, ) async { - final isMobile = UniversalPlatform.isMobile; // the url should contain a protocol if (text == null || !isURL(text, {'require_protocol': true})) { return false; @@ -186,7 +184,7 @@ Future _pasteAsLinkPreview( node.delta?.toPlainText().isNotEmpty == true) { return false; } - if (!isMobile) return false; + final bool isImageUrl; try { isImageUrl = await _isImageUrl(text); @@ -195,8 +193,6 @@ Future _pasteAsLinkPreview( return false; } - if (!isImageUrl) return false; - // insert the text with link format final textTransaction = editorState.transaction ..insertText( @@ -254,7 +250,6 @@ Future doPlainPaste(EditorState editorState) async { } Future _isImageUrl(String text) async { - if (isNotImageUrl(text)) return false; final response = await http.head(Uri.parse(text)); if (response.statusCode == 200) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart index c47c0c967d..fbd9914c1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart @@ -43,11 +43,13 @@ extension PasteFromBlockLink on EditorState { node, selection.startIndex, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: pageId, - blockId: blockId, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.blockId: blockId, + MentionBlockKeys.pageId: pageId, + }, + }, ); await apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart index 3f11759545..6eff666991 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; @@ -14,7 +13,6 @@ extension PasteFromHtml on EditorState { } if (nodes.length == 1) { await pasteSingleLineNode(nodes.first); - checkToShowPasteAsMenu(nodes.first); } else { await pasteMultiLineNodes(nodes.toList()); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index d086f36bed..6e6c9b1772 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -73,6 +73,7 @@ extension PasteFromImage on EditorState { Log.info('unsupported format: $format'); if (UniversalPlatform.isMobile) { showToastNotification( + context, message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), ); } @@ -111,6 +112,7 @@ extension PasteFromImage on EditorState { if (errorMessage != null && context.mounted) { showToastNotification( + context, message: errorMessage, ); return false; @@ -129,6 +131,7 @@ extension PasteFromImage on EditorState { Log.error('cannot copy image file', e); if (context.mounted) { showToastNotification( + context, message: LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart index fcb12cefa5..90ed451128 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart @@ -1,8 +1,6 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:universal_platform/universal_platform.dart'; extension PasteFromPlainText on EditorState { Future pastePlainText(String plainText) async { @@ -34,16 +32,12 @@ extension PasteFromPlainText on EditorState { await deleteSelectionIfNeeded(); - /// try to parse the plain text as markdown final nodes = customMarkdownToDocument(plainText).root.children; if (nodes.isEmpty) { - /// if the markdown parser failed, fallback to the plain text parser - await pastePlainText(plainText); return; } if (nodes.length == 1) { await pasteSingleLineNode(nodes.first); - checkToShowPasteAsMenu(nodes.first); } else { await pasteMultiLineNodes(nodes.toList()); } @@ -68,29 +62,6 @@ extension PasteFromPlainText on EditorState { AppFlowyRichTextKeys.href: plainText, }); await apply(transaction); - checkToShowPasteAsMenu(node); return true; } - - void checkToShowPasteAsMenu(Node node) { - if (selection == null || !selection!.isCollapsed) return; - if (UniversalPlatform.isMobile) return; - final href = _getLinkFromNode(node); - if (href != null) { - final context = document.root.context; - if (context != null && context.mounted) { - PasteAsMenuService(context: context, editorState: this).show(href); - } - } - } - - String? _getLinkFromNode(Node node) { - final delta = node.delta; - if (delta == null) return null; - final inserts = delta.whereType(); - if (inserts.isEmpty || inserts.length > 1) return null; - final link = inserts.first.attributes?.href; - if (link != null) return inserts.first.text; - return null; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart index 3006fc3104..ee8952dc11 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart @@ -1,7 +1,6 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,13 +19,9 @@ class DocumentImmersiveCoverBloc (event, emit) async { await event.when( initial: () async { - final latestView = await ViewBackendService.getView(view.id); add( DocumentImmersiveCoverEvent.updateCoverAndIcon( - latestView.fold( - (s) => s.cover, - (e) => view.cover, - ), + view.cover, EmojiIconData.fromViewIconPB(view.icon), view.name, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart index 87c2815091..86df2ae172 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart @@ -42,10 +42,6 @@ class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -62,7 +58,6 @@ class DatabaseBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart index 905c033bda..5beba66c32 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -28,15 +27,9 @@ extension TextDeltaExtension on Delta { if (op.text == MentionBlockKeys.mentionChar) { final mention = attributes?[MentionBlockKeys.mention]; final mentionPageId = mention?[MentionBlockKeys.pageId]; - final mentionType = mention?[MentionBlockKeys.type]; if (mentionPageId != null) { text += await getMentionPageName(mentionPageId); continue; - } else if (mentionType == MentionType.externalLink.name) { - final url = mention?[MentionBlockKeys.url] ?? ''; - final info = await LinkInfoCache.get(url); - text += info?.title ?? url; - continue; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart deleted file mode 100644 index 162c7a1c34..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/overlay_util.dart'; -import 'package:flutter/material.dart'; - -class ColorPicker extends StatefulWidget { - const ColorPicker({ - super.key, - required this.title, - required this.selectedColorHex, - required this.onSubmittedColorHex, - required this.colorOptions, - this.resetText, - this.customColorHex, - this.resetIconName, - this.showClearButton = false, - }); - - final String title; - final String? selectedColorHex; - final String? customColorHex; - final void Function(String? color, bool isCustomColor) onSubmittedColorHex; - final String? resetText; - final String? resetIconName; - final bool showClearButton; - - final List colorOptions; - - @override - State createState() => _ColorPickerState(); -} - -class _ColorPickerState extends State { - final TextEditingController _colorHexController = TextEditingController(); - final TextEditingController _colorOpacityController = TextEditingController(); - - @override - void initState() { - super.initState(); - final selectedColorHex = widget.selectedColorHex, - customColorHex = widget.customColorHex; - _colorHexController.text = - _extractColorHex(customColorHex ?? selectedColorHex) ?? 'FFFFFF'; - _colorOpacityController.text = - _convertHexToOpacity(customColorHex ?? selectedColorHex) ?? '100'; - } - - @override - Widget build(BuildContext context) { - return basicOverlay( - context, - width: 300, - height: 250, - children: [ - EditorOverlayTitle(text: widget.title), - const SizedBox(height: 6), - widget.showClearButton && - widget.resetText != null && - widget.resetIconName != null - ? ResetColorButton( - resetText: widget.resetText!, - resetIconName: widget.resetIconName!, - onPressed: (color) => - widget.onSubmittedColorHex.call(color, false), - ) - : const SizedBox.shrink(), - CustomColorItem( - colorController: _colorHexController, - opacityController: _colorOpacityController, - onSubmittedColorHex: (color) => - widget.onSubmittedColorHex.call(color, true), - ), - _buildColorItems( - widget.colorOptions, - widget.selectedColorHex, - ), - ], - ); - } - - Widget _buildColorItems( - List options, - String? selectedColor, - ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: options - .map((e) => _buildColorItem(e, e.colorHex == selectedColor)) - .toList(), - ); - } - - Widget _buildColorItem(ColorOption option, bool isChecked) { - return SizedBox( - height: 36, - child: TextButton.icon( - onPressed: () { - widget.onSubmittedColorHex(option.colorHex, false); - }, - icon: SizedBox.square( - dimension: 12, - child: Container( - decoration: BoxDecoration( - color: option.colorHex.tryToColor(), - shape: BoxShape.circle, - ), - ), - ), - style: buildOverlayButtonStyle(context), - label: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - option.name, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - style: TextStyle( - color: Theme.of(context).textTheme.labelLarge?.color, - ), - ), - ), - // checkbox - if (isChecked) const FlowySvg(FlowySvgs.toolbar_check_m), - ], - ), - ), - ); - } - - String? _convertHexToOpacity(String? colorHex) { - if (colorHex == null) return null; - final opacityHex = colorHex.substring(2, 4); - final opacity = int.parse(opacityHex, radix: 16) / 2.55; - return opacity.toStringAsFixed(0); - } - - String? _extractColorHex(String? colorHex) { - if (colorHex == null) return null; - return colorHex.substring(4); - } -} - -class ResetColorButton extends StatelessWidget { - const ResetColorButton({ - super.key, - required this.resetText, - required this.resetIconName, - required this.onPressed, - }); - - final Function(String? color) onPressed; - final String resetText; - final String resetIconName; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 32, - child: TextButton.icon( - onPressed: () => onPressed(null), - icon: EditorSvg( - name: resetIconName, - width: 13, - height: 13, - color: Theme.of(context).iconTheme.color, - ), - label: Text( - resetText, - style: TextStyle( - color: Theme.of(context).hintColor, - ), - textAlign: TextAlign.left, - ), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.hovered)) { - return Theme.of(context).hoverColor; - } - return Colors.transparent; - }, - ), - alignment: Alignment.centerLeft, - ), - ), - ); - } -} - -class CustomColorItem extends StatefulWidget { - const CustomColorItem({ - super.key, - required this.colorController, - required this.opacityController, - required this.onSubmittedColorHex, - }); - - final TextEditingController colorController; - final TextEditingController opacityController; - final void Function(String color) onSubmittedColorHex; - - @override - State createState() => _CustomColorItemState(); -} - -class _CustomColorItemState extends State { - @override - Widget build(BuildContext context) { - return ExpansionTile( - tilePadding: const EdgeInsets.only(left: 8), - shape: Border.all( - color: Colors.transparent, - ), // remove the default border when it is expanded - title: Row( - children: [ - // color sample box - SizedBox.square( - dimension: 12, - child: Container( - decoration: BoxDecoration( - color: Color( - int.tryParse( - _combineColorHexAndOpacity( - widget.colorController.text, - widget.opacityController.text, - ), - ) ?? - 0xFFFFFFFF, - ), - shape: BoxShape.circle, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - AppFlowyEditorL10n.current.customColor, - style: Theme.of(context).textTheme.labelLarge, - // same style as TextButton.icon - ), - ), - ], - ), - children: [ - const SizedBox(height: 6), - _customColorDetailsTextField( - labelText: AppFlowyEditorL10n.current.hexValue, - controller: widget.colorController, - // update the color sample box when the text changes - onChanged: (_) => setState(() {}), - onSubmitted: _submitCustomColorHex, - ), - const SizedBox(height: 10), - _customColorDetailsTextField( - labelText: AppFlowyEditorL10n.current.opacity, - controller: widget.opacityController, - // update the color sample box when the text changes - onChanged: (_) => setState(() {}), - onSubmitted: _submitCustomColorHex, - ), - const SizedBox(height: 6), - ], - ); - } - - Widget _customColorDetailsTextField({ - required String labelText, - required TextEditingController controller, - Function(String)? onChanged, - Function(String)? onSubmitted, - }) { - return Padding( - padding: const EdgeInsets.only(right: 3), - child: TextField( - controller: controller, - decoration: InputDecoration( - labelText: labelText, - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - style: Theme.of(context).textTheme.bodyMedium, - onChanged: onChanged, - onSubmitted: onSubmitted, - ), - ); - } - - String _combineColorHexAndOpacity(String colorHex, String opacity) { - colorHex = _fixColorHex(colorHex); - opacity = _fixOpacity(opacity); - final opacityHex = (int.parse(opacity) * 2.55).round().toRadixString(16); - return '0x$opacityHex$colorHex'; - } - - String _fixColorHex(String colorHex) { - if (colorHex.length > 6) { - colorHex = colorHex.substring(0, 6); - } - if (int.tryParse(colorHex, radix: 16) == null) { - colorHex = 'FFFFFF'; - } - return colorHex; - } - - String _fixOpacity(String opacity) { - // if opacity is 0 - 99, return it - // otherwise return 100 - final RegExp regex = RegExp('^(0|[1-9][0-9]?)'); - if (regex.hasMatch(opacity)) { - return opacity; - } else { - return '100'; - } - } - - void _submitCustomColorHex(String value) { - final String color = _combineColorHexAndOpacity( - widget.colorController.text, - widget.opacityController.text, - ); - widget.onSubmittedColorHex(color); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart deleted file mode 100644 index 03fc12a37c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_animation.dart'; - -class DesktopFloatingToolbar extends StatefulWidget { - const DesktopFloatingToolbar({ - super.key, - required this.editorState, - required this.child, - required this.onDismiss, - this.enableAnimation = true, - }); - - final EditorState editorState; - final Widget child; - final VoidCallback onDismiss; - final bool enableAnimation; - - @override - State createState() => _DesktopFloatingToolbarState(); -} - -class _DesktopFloatingToolbarState extends State { - EditorState get editorState => widget.editorState; - - _Position? position; - final toolbarController = getIt(); - - @override - void initState() { - super.initState(); - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return; - } - final selectionRect = editorState.selectionRects(); - if (selectionRect.isEmpty) return; - position = calculateSelectionMenuOffset(selectionRect.first); - toolbarController._addCallback(dismiss); - } - - @override - void dispose() { - toolbarController._removeCallback(dismiss); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (position == null) return Container(); - return Positioned( - left: position!.left, - top: position!.top, - right: position!.right, - child: widget.enableAnimation - ? ToolbarAnimationWidget(child: widget.child) - : widget.child, - ); - } - - void dismiss() { - widget.onDismiss.call(); - } - - _Position calculateSelectionMenuOffset( - Rect rect, - ) { - const toolbarHeight = 40, topLimit = toolbarHeight + 8; - final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState); - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorSize = editorState.renderBox?.size ?? Size.zero; - final menuWidth = - isLongMenu ? (isNarrowWindow(editorState) ? 490.0 : 660.0) : 420.0; - final editorRect = editorOffset & editorSize; - final left = rect.left, leftStart = 50; - final top = - rect.top < topLimit ? rect.bottom + topLimit : rect.top - topLimit; - if (left + menuWidth > editorRect.right) { - return _Position( - editorRect.right - menuWidth, - top, - null, - ); - } else if (rect.left - leftStart > 0) { - return _Position(rect.left - leftStart, top, null); - } else { - return _Position(rect.left, top, null); - } - } -} - -class _Position { - _Position(this.left, this.top, this.right); - - final double? left; - final double? top; - final double? right; -} - -class FloatingToolbarController { - final Set _dismissCallbacks = {}; - final Set _displayListeners = {}; - - void _addCallback(VoidCallback callback) { - _dismissCallbacks.add(callback); - for (final listener in Set.of(_displayListeners)) { - listener.call(); - } - } - - void _removeCallback(VoidCallback callback) => - _dismissCallbacks.remove(callback); - - bool get isToolbarShowing => _dismissCallbacks.isNotEmpty; - - void addDisplayListener(VoidCallback listener) => - _displayListeners.add(listener); - - void removeDisplayListener(VoidCallback listener) => - _displayListeners.remove(listener); - - void hideToolbar() { - if (_dismissCallbacks.isEmpty) return; - for (final callback in _dismissCallbacks) { - callback.call(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart deleted file mode 100644 index 002d569c7b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'link_search_text_field.dart'; - -class LinkCreateMenu extends StatefulWidget { - const LinkCreateMenu({ - super.key, - required this.editorState, - required this.onSubmitted, - required this.onDismiss, - required this.alignment, - required this.currentViewId, - required this.initialText, - }); - - final EditorState editorState; - final void Function(String link, bool isPage) onSubmitted; - final VoidCallback onDismiss; - final String currentViewId; - final String initialText; - final LinkMenuAlignment alignment; - - @override - State createState() => _LinkCreateMenuState(); -} - -class _LinkCreateMenuState extends State { - late LinkSearchTextField searchTextField = LinkSearchTextField( - currentViewId: widget.currentViewId, - initialSearchText: widget.initialText, - onEnter: () { - searchTextField.onSearchResult( - onLink: () => onSubmittedLink(), - onRecentViews: () => - onSubmittedPageLink(searchTextField.currentRecentView), - onSearchViews: () => - onSubmittedPageLink(searchTextField.currentSearchedView), - onEmpty: () {}, - ); - }, - onEscape: widget.onDismiss, - onDataRefresh: () { - if (mounted) setState(() {}); - }, - ); - - bool get isTextfieldEnable => searchTextField.isTextfieldEnable; - - String get searchText => searchTextField.searchText; - - bool get showAtTop => widget.alignment.isTop; - - bool showErrorText = false; - - @override - void initState() { - super.initState(); - searchTextField.requestFocus(); - searchTextField.searchRecentViews(); - final focusNode = searchTextField.focusNode; - bool hasFocus = focusNode.hasFocus; - focusNode.addListener(() { - if (hasFocus != focusNode.hasFocus && mounted) { - setState(() { - hasFocus = focusNode.hasFocus; - }); - } - }); - } - - @override - void dispose() { - searchTextField.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 320, - child: Column( - children: showAtTop - ? [ - searchTextField.buildResultContainer( - margin: EdgeInsets.only(bottom: 2), - context: context, - onLinkSelected: onSubmittedLink, - onPageLinkSelected: onSubmittedPageLink, - ), - buildSearchContainer(), - ] - : [ - buildSearchContainer(), - searchTextField.buildResultContainer( - margin: EdgeInsets.only(top: 2), - context: context, - onLinkSelected: onSubmittedLink, - onPageLinkSelected: onSubmittedPageLink, - ), - ], - ), - ); - } - - Widget buildSearchContainer() { - return Container( - width: 320, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.all(8), - child: ValueListenableBuilder( - valueListenable: searchTextField.textEditingController, - builder: (context, _, __) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: searchTextField.buildTextField(context: context), - ), - HSpace(8), - FlowyTextButton( - LocaleKeys.document_toolbar_insert.tr(), - mainAxisAlignment: MainAxisAlignment.center, - padding: EdgeInsets.zero, - constraints: BoxConstraints(maxWidth: 72, minHeight: 32), - fontSize: 14, - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - lineHeight: 20 / 14, - fontWeight: FontWeight.w600, - onPressed: onSubmittedLink, - ), - ], - ), - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - }, - ), - ); - } - - void onSubmittedLink() { - if (!isTextfieldEnable) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onSubmitted(searchText, false); - } - - void onSubmittedPageLink(ViewPB view) async { - final workspaceId = context - .read() - ?.state - .currentWorkspace - ?.workspaceId ?? - ''; - final link = ShareConstants.buildShareUrl( - workspaceId: workspaceId, - viewId: view.id, - ); - widget.onSubmitted(link, true); - } -} - -void showLinkCreateMenu( - BuildContext context, - EditorState editorState, - Selection selection, - String currentViewId, -) { - if (!context.mounted) return; - final (left, top, right, bottom, alignment) = _getPosition(editorState); - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final selectedText = editorState.getTextInSelection(selection).join(); - - OverlayEntry? overlay; - - void dismissOverlay() { - keepEditorFocusNotifier.decrease(); - overlay?.remove(); - overlay = null; - } - - keepEditorFocusNotifier.increase(); - overlay = FullScreenOverlayEntry( - top: top, - bottom: bottom, - left: left, - right: right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) { - return LinkCreateMenu( - alignment: alignment, - initialText: selectedText, - currentViewId: currentViewId, - editorState: editorState, - onSubmitted: (link, isPage) async { - await editorState.formatDelta(selection, { - BuiltInAttributeKey.href: link, - kIsPageLink: isPage, - }); - await editorState.updateSelectionWithReason( - null, - reason: SelectionUpdateReason.uiEvent, - ); - dismissOverlay(); - }, - onDismiss: dismissOverlay, - ); - }, - ).build(); - - Overlay.of(context, rootOverlay: true).insert(overlay!); -} - -// get a proper position for link menu -( - double? left, - double? top, - double? right, - double? bottom, - LinkMenuAlignment alignment, -) _getPosition( - EditorState editorState, -) { - final rect = editorState.selectionRects().first; - const menuHeight = 222.0, menuWidth = 320.0; - - double? left, right, top, bottom; - LinkMenuAlignment alignment = LinkMenuAlignment.topLeft; - final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero), - editorSize = editorState.renderBox!.size; - final editorBottom = editorSize.height + editorOffset.dy, - editorRight = editorSize.width + editorOffset.dx; - final overflowBottom = rect.bottom + menuHeight > editorBottom, - overflowTop = rect.top - menuHeight < 0, - overflowLeft = rect.left - menuWidth < 0, - overflowRight = rect.right + menuWidth > editorRight; - - if (overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else if (overflowBottom && !overflowTop) { - /// show at top - bottom = editorBottom - rect.top; - } else if (!overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else { - top = 0; - } - - if (overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else if (overflowRight && !overflowLeft) { - /// show at left - right = editorRight - rect.right; - } else if (!overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else { - left = 0; - } - - if (left != null && top != null) { - alignment = LinkMenuAlignment.bottomRight; - } else if (left != null && bottom != null) { - alignment = LinkMenuAlignment.topRight; - } else if (right != null && top != null) { - alignment = LinkMenuAlignment.bottomLeft; - } else if (right != null && bottom != null) { - alignment = LinkMenuAlignment.topLeft; - } - - return (left, top, right, bottom, alignment); -} - -ShapeDecoration buildToolbarLinkDecoration( - BuildContext context, { - double radius = 12.0, -}) { - final theme = AppFlowyTheme.of(context); - return ShapeDecoration( - color: theme.surfaceColorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius), - ), - shadows: theme.shadow.small, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart deleted file mode 100644 index e90ee22a80..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart +++ /dev/null @@ -1,516 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.dart'; -import 'package:flutter/services.dart'; -import 'link_create_menu.dart'; -import 'link_search_text_field.dart'; -import 'link_styles.dart'; - -class LinkEditMenu extends StatefulWidget { - const LinkEditMenu({ - super.key, - required this.linkInfo, - required this.onDismiss, - required this.onApply, - required this.onRemoveLink, - required this.currentViewId, - }); - - final LinkInfo linkInfo; - final ValueChanged onApply; - final ValueChanged onRemoveLink; - final VoidCallback onDismiss; - final String currentViewId; - - @override - State createState() => _LinkEditMenuState(); -} - -class _LinkEditMenuState extends State { - ValueChanged get onRemoveLink => widget.onRemoveLink; - - VoidCallback get onDismiss => widget.onDismiss; - - late TextEditingController linkNameController = - TextEditingController(text: linkInfo.name); - late FocusNode textFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); - late FocusNode menuFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); - late LinkInfo linkInfo = widget.linkInfo; - late LinkSearchTextField searchTextField; - bool isShowingSearchResult = false; - ViewPB? currentView; - bool showErrorText = false; - - @override - void initState() { - super.initState(); - final isPageLink = linkInfo.isPage; - if (isPageLink) getPageView(); - searchTextField = LinkSearchTextField( - initialSearchText: isPageLink ? '' : linkInfo.link, - initialViewId: linkInfo.viewId, - currentViewId: widget.currentViewId, - onEnter: onConfirm, - onEscape: () { - if (isShowingSearchResult) { - hideSearchResult(); - } else { - onDismiss(); - } - }, - onDataRefresh: () { - if (mounted) setState(() {}); - }, - )..searchRecentViews(); - makeSureHasFocus(); - } - - @override - void dispose() { - linkNameController.dispose(); - textFocusNode.dispose(); - menuFocusNode.dispose(); - searchTextField.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final showingRecent = - searchTextField.showingRecent && isShowingSearchResult; - final errorHeight = showErrorText ? 20.0 : 0.0; - return GestureDetector( - onTap: onDismiss, - child: Focus( - focusNode: menuFocusNode, - child: Container( - width: 400, - height: 250 + (showingRecent ? 32 : 0), - color: Colors.white.withAlpha(1), - child: Stack( - children: [ - GestureDetector( - onTap: hideSearchResult, - child: Container( - width: 400, - height: 192 + errorHeight, - decoration: buildToolbarLinkDecoration(context), - ), - ), - Positioned( - top: 16, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_pageOrURL.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Positioned( - top: 80 + errorHeight, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_linkName.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Positioned( - top: 144 + errorHeight, - left: 20, - child: buildButtons(), - ), - Positioned( - top: 100 + errorHeight, - left: 20, - child: buildNameTextField(), - ), - Positioned( - top: 36, - left: 20, - child: buildLinkField(), - ), - ], - ), - ), - ), - ); - } - - Widget buildLinkField() { - final showPageView = linkInfo.isPage && !isShowingSearchResult; - Widget child; - if (showPageView) { - child = buildPageView(); - } else if (!isShowingSearchResult) { - child = buildLinkView(); - } else { - return SizedBox( - width: 360, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 360, - height: 32, - child: searchTextField.buildTextField( - autofocus: true, - context: context, - ), - ), - VSpace(6), - searchTextField.buildResultContainer( - context: context, - width: 360, - onPageLinkSelected: onPageSelected, - onLinkSelected: onLinkSelected, - ), - ], - ), - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - child, - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - } - - Widget buildButtons() { - return GestureDetector( - onTap: hideSearchResult, - child: SizedBox( - width: 360, - height: 32, - child: Row( - children: [ - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), - width: 32, - height: 32, - tooltipText: LocaleKeys.editor_removeLink.tr(), - preferBelow: false, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor(context)), - ), - onPressed: () => onRemoveLink.call(linkInfo), - ), - Spacer(), - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor(context)), - ), - child: FlowyTextButton( - LocaleKeys.button_cancel.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - fontColor: Theme.of(context).isLightMode - ? LinkStyle.textPrimary - : Theme.of(context).iconTheme.color, - fillColor: Colors.transparent, - fontWeight: FontWeight.w400, - onPressed: onDismiss, - ), - ), - HSpace(12), - ValueListenableBuilder( - valueListenable: linkNameController, - builder: (context, _, __) { - return FlowyTextButton( - LocaleKeys.settings_appearance_documentSettings_apply.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - fontWeight: FontWeight.w400, - onPressed: onApply, - ); - }, - ), - ], - ), - ), - ); - } - - Widget buildNameTextField() { - return SizedBox( - width: 360, - height: 32, - child: TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - focusNode: textFocusNode, - autofocus: true, - textAlign: TextAlign.left, - controller: linkNameController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - onChanged: (text) { - linkInfo = LinkInfo( - name: text, - link: linkInfo.link, - isPage: linkInfo.isPage, - ); - }, - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_toolbar_linkNameHint.tr(), - context, - ), - ), - ); - } - - Widget buildPageView() { - late Widget child; - final view = currentView; - if (view == null) { - child = Center( - child: SizedBox.fromSize( - size: Size(10, 10), - child: CircularProgressIndicator(), - ), - ); - } else { - final viewName = view.name; - final displayName = viewName.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : viewName; - child = GestureDetector( - onTap: showSearchResult, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyTooltip( - preferBelow: false, - message: displayName, - child: Container( - height: 32, - padding: EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Row( - children: [ - searchTextField.buildIcon(view), - HSpace(4), - Flexible( - child: FlowyText.regular( - displayName, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ), - ], - ), - ), - ), - ), - ); - } - return Container( - width: 360, - height: 32, - decoration: buildDecoration(), - child: child, - ); - } - - Widget buildLinkView() { - return Container( - width: 360, - height: 32, - decoration: buildDecoration(), - child: FlowyTooltip( - preferBelow: false, - message: linkInfo.link, - child: GestureDetector( - onTap: showSearchResult, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Row( - children: [ - FlowySvg(FlowySvgs.toolbar_link_earth_m), - HSpace(8), - Flexible( - child: FlowyText.regular( - linkInfo.link, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - if (key.logicalKey == LogicalKeyboardKey.enter) { - onApply(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.escape) { - onDismiss(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - Future makeSureHasFocus() async { - final focusNode = textFocusNode; - if (!mounted || focusNode.hasFocus) return; - focusNode.requestFocus(); - WidgetsBinding.instance.addPostFrameCallback((_) { - makeSureHasFocus(); - }); - } - - void onApply() { - if (isShowingSearchResult) { - onConfirm(); - return; - } - if (linkInfo.link.isEmpty) { - widget.onRemoveLink(linkInfo); - return; - } - if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onApply.call(linkInfo); - } - - void onConfirm() { - searchTextField.onSearchResult( - onLink: onLinkSelected, - onRecentViews: () => onPageSelected(searchTextField.currentRecentView), - onSearchViews: () => onPageSelected(searchTextField.currentSearchedView), - onEmpty: () { - searchTextField.unfocus(); - }, - ); - menuFocusNode.requestFocus(); - } - - Future getPageView() async { - if (!linkInfo.isPage) return; - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(linkInfo.viewId); - if (mounted) { - setState(() { - currentView = view; - }); - } - } - - void showSearchResult() { - setState(() { - if (linkInfo.isPage) searchTextField.updateText(''); - isShowingSearchResult = true; - searchTextField.requestFocus(); - }); - } - - void hideSearchResult() { - setState(() { - isShowingSearchResult = false; - searchTextField.unfocus(); - textFocusNode.unfocus(); - }); - } - - void onLinkSelected() { - if (mounted) { - linkInfo = LinkInfo( - name: linkInfo.name, - link: searchTextField.searchText, - ); - hideSearchResult(); - } - } - - Future onPageSelected(ViewPB view) async { - currentView = view; - final link = ShareConstants.buildShareUrl( - workspaceId: await UserBackendService.getCurrentWorkspace().fold( - (s) => s.id, - (f) => '', - ), - viewId: view.id, - ); - linkInfo = LinkInfo( - name: linkInfo.name, - link: link, - isPage: true, - ); - searchTextField.updateText(linkInfo.link); - if (mounted) { - setState(() { - isShowingSearchResult = false; - searchTextField.unfocus(); - }); - } - } - - BoxDecoration buildDecoration() => BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: LinkStyle.borderColor(context)), - ); -} - -class LinkInfo { - LinkInfo({this.isPage = false, required this.name, required this.link}); - - final bool isPage; - final String name; - final String link; - - Attributes toAttribute() => - {AppFlowyRichTextKeys.href: link, kIsPageLink: isPage}; - - String get viewId => isPage ? link.split('/').lastOrNull ?? '' : ''; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart deleted file mode 100644 index c992e40c61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ /dev/null @@ -1,635 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'link_create_menu.dart'; -import 'link_edit_menu.dart'; - -class LinkHoverTrigger extends StatefulWidget { - const LinkHoverTrigger({ - super.key, - required this.editorState, - required this.selection, - required this.node, - required this.attribute, - required this.size, - this.delayToShow = const Duration(milliseconds: 50), - this.delayToHide = const Duration(milliseconds: 300), - }); - - final EditorState editorState; - final Selection selection; - final Node node; - final Attributes attribute; - final Size size; - final Duration delayToShow; - final Duration delayToHide; - - @override - State createState() => _LinkHoverTriggerState(); -} - -class _LinkHoverTriggerState extends State { - final hoverMenuController = PopoverController(); - final editMenuController = PopoverController(); - final toolbarController = getIt(); - bool isHoverMenuShowing = false; - bool isHoverMenuHovering = false; - bool isHoverTriggerHovering = false; - - Size get size => widget.size; - - EditorState get editorState => widget.editorState; - - Selection get selection => widget.selection; - - Attributes get attribute => widget.attribute; - - late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection); - - @override - void initState() { - super.initState(); - getIt()._add(triggerKey, showLinkHoverMenu); - toolbarController.addDisplayListener(onToolbarShow); - } - - @override - void dispose() { - hoverMenuController.close(); - editMenuController.close(); - getIt()._remove(triggerKey, showLinkHoverMenu); - toolbarController.removeDisplayListener(onToolbarShow); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (v) { - isHoverTriggerHovering = true; - Future.delayed(widget.delayToShow, () { - if (isHoverTriggerHovering && !isHoverMenuShowing) { - showLinkHoverMenu(); - } - }); - }, - onExit: (v) { - isHoverTriggerHovering = false; - tryToDismissLinkHoverMenu(); - }, - child: buildHoverPopover( - buildEditPopover( - Container( - color: Colors.black.withAlpha(1), - width: size.width, - height: size.height, - ), - ), - ), - ); - } - - Widget buildHoverPopover(Widget child) { - return AppFlowyPopover( - controller: hoverMenuController, - direction: PopoverDirection.topWithLeftAligned, - offset: Offset(0, size.height), - onOpen: () { - keepEditorFocusNotifier.increase(); - isHoverMenuShowing = true; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - isHoverMenuShowing = false; - }, - margin: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: max(320, size.width), - maxHeight: 48 + size.height, - ), - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - popupBuilder: (context) => LinkHoverMenu( - attribute: widget.attribute, - triggerSize: size, - onEnter: (_) { - isHoverMenuHovering = true; - }, - onExit: (_) { - isHoverMenuHovering = false; - tryToDismissLinkHoverMenu(); - }, - onConvertTo: (type) => convertLinkTo(editorState, selection, type), - onOpenLink: openLink, - onCopyLink: () => copyLink(context), - onEditLink: showLinkEditMenu, - onRemoveLink: () => removeLink(editorState, selection), - ), - child: child, - ); - } - - Widget buildEditPopover(Widget child) { - final href = attribute.href ?? '', - isPage = attribute.isPage, - title = editorState.getTextInSelection(selection).join(); - final currentViewId = context.read()?.documentId ?? ''; - return AppFlowyPopover( - controller: editMenuController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, 0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - margin: EdgeInsets.zero, - asBarrier: true, - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - constraints: BoxConstraints( - maxWidth: 400, - minHeight: 282, - ), - popupBuilder: (context) => LinkEditMenu( - currentViewId: currentViewId, - linkInfo: LinkInfo(name: title, link: href, isPage: isPage), - onDismiss: () => editMenuController.close(), - onApply: (info) async { - final transaction = editorState.transaction; - transaction.replaceText( - widget.node, - selection.startIndex, - selection.length, - info.name, - attributes: info.toAttribute(), - ); - editMenuController.close(); - await editorState.apply(transaction); - }, - onRemoveLink: (linkinfo) => - onRemoveAndReplaceLink(editorState, selection, linkinfo.name), - ), - child: child, - ); - } - - void onToolbarShow() => hoverMenuController.close(); - - void showLinkHoverMenu() { - if (isHoverMenuShowing || toolbarController.isToolbarShowing || !mounted) { - return; - } - keepEditorFocusNotifier.increase(); - hoverMenuController.show(); - } - - void showLinkEditMenu() { - keepEditorFocusNotifier.increase(); - hoverMenuController.close(); - editMenuController.show(); - } - - void tryToDismissLinkHoverMenu() { - Future.delayed(widget.delayToHide, () { - if (isHoverMenuHovering || isHoverTriggerHovering) { - return; - } - hoverMenuController.close(); - }); - } - - Future openLink() async { - final href = widget.attribute.href ?? '', isPage = widget.attribute.isPage; - - if (isPage) { - final viewId = href.split('/').lastOrNull ?? ''; - if (viewId.isEmpty) { - await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); - } else { - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(viewId); - if (view != null) { - await handleMentionBlockTap(context, widget.editorState, view); - } - } - } else { - await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); - } - } - - Future copyLink(BuildContext context) async { - final href = widget.attribute.href ?? ''; - await context.copyLink(href); - hoverMenuController.close(); - } - - void removeLink( - EditorState editorState, - Selection selection, - ) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..formatText( - node, - index, - length, - { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); - } - - Future convertLinkTo( - EditorState editorState, - Selection selection, - LinkConvertMenuCommand type, - ) async { - final url = widget.attribute.href ?? ''; - if (type == LinkConvertMenuCommand.toBookmark) { - await convertUrlToLinkPreview(editorState, selection, url); - } else if (type == LinkConvertMenuCommand.toMention) { - await convertUrlToMention(editorState, selection); - } else if (type == LinkConvertMenuCommand.toEmbed) { - await convertUrlToLinkPreview( - editorState, - selection, - url, - previewType: LinkEmbedKeys.embed, - ); - } - } - - void onRemoveAndReplaceLink( - EditorState editorState, - Selection selection, - String text, - ) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..replaceText( - node, - index, - length, - text, - attributes: { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); - } -} - -class LinkHoverMenu extends StatefulWidget { - const LinkHoverMenu({ - super.key, - required this.attribute, - required this.onEnter, - required this.onExit, - required this.triggerSize, - required this.onCopyLink, - required this.onOpenLink, - required this.onEditLink, - required this.onRemoveLink, - required this.onConvertTo, - }); - - final Attributes attribute; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final Size triggerSize; - final VoidCallback onCopyLink; - final VoidCallback onOpenLink; - final VoidCallback onEditLink; - final VoidCallback onRemoveLink; - final ValueChanged onConvertTo; - - @override - State createState() => _LinkHoverMenuState(); -} - -class _LinkHoverMenuState extends State { - ViewPB? currentView; - late bool isPage = widget.attribute.isPage; - late String href = widget.attribute.href ?? ''; - final popoverController = PopoverController(); - bool isConvertButtonSelected = false; - - @override - void initState() { - super.initState(); - if (isPage) getPageView(); - } - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: SizedBox( - width: max(320, widget.triggerSize.width), - height: 48, - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: 320, - height: 48, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.fromLTRB(12, 8, 8, 8), - child: Row( - children: [ - Expanded(child: buildLinkWidget()), - Container( - height: 20, - width: 1, - color: Color(0xffE8ECF3) - .withAlpha(Theme.of(context).isLightMode ? 255 : 40), - margin: EdgeInsets.symmetric(horizontal: 6), - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_m), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onCopyLink, - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_edit_m), - tooltipText: LocaleKeys.editor_editLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onEditLink, - ), - buildConvertButton(), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), - tooltipText: LocaleKeys.editor_removeLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onRemoveLink, - ), - ], - ), - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - onTap: widget.onOpenLink, - child: Container( - width: widget.triggerSize.width, - height: widget.triggerSize.height, - color: Colors.black.withAlpha(1), - ), - ), - ), - ], - ); - } - - Future getPageView() async { - final viewId = href.split('/').lastOrNull ?? ''; - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(viewId); - if (mounted) { - setState(() { - currentView = view; - }); - } - } - - Widget buildLinkWidget() { - final view = currentView; - if (isPage && view == null) { - return SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ); - } - String text = ''; - if (isPage && view != null) { - text = view.name; - if (text.isEmpty) { - text = LocaleKeys.document_title_placeholder.tr(); - } - } else { - text = href; - } - return FlowyTooltip( - message: text, - preferBelow: false, - child: FlowyText.regular( - text, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ); - } - - Widget buildConvertButton() { - return AppFlowyPopover( - offset: Offset(44, 10.0), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg(FlowySvgs.turninto_m), - isSelected: isConvertButtonSelected, - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: () { - setState(() { - isConvertButtonSelected = true; - }); - showConvertMenu(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(LinkConvertMenuCommand.values.length, (index) { - final command = LinkConvertMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () { - widget.onConvertTo(command); - closeConvertMenu(); - }, - ), - ); - }), - ), - ), - ); - } - - void showConvertMenu() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void closeConvertMenu() { - popoverController.close(); - } -} - -class HoverTriggerKey { - HoverTriggerKey(this.nodeId, this.selection); - - final String nodeId; - final Selection selection; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HoverTriggerKey && - runtimeType == other.runtimeType && - nodeId == other.nodeId && - isSelectionSame(other.selection); - - bool isSelectionSame(Selection other) => - (selection.start == other.start && selection.end == other.end) || - (selection.start == other.end && selection.end == other.start); - - @override - int get hashCode => nodeId.hashCode ^ selection.hashCode; -} - -class LinkHoverTriggers { - final Map> _map = {}; - - void _add(HoverTriggerKey key, VoidCallback callback) { - final callbacks = _map[key] ?? {}; - callbacks.add(callback); - _map[key] = callbacks; - } - - void _remove(HoverTriggerKey key, VoidCallback callback) { - final callbacks = _map[key] ?? {}; - callbacks.remove(callback); - _map[key] = callbacks; - } - - void call(HoverTriggerKey key) { - final callbacks = _map[key] ?? {}; - if (callbacks.isEmpty) return; - callbacks.first.call(); - } -} - -enum LinkConvertMenuCommand { - toMention, - toBookmark, - toEmbed; - - String get title { - switch (this) { - case toMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - } - } - - String get type { - switch (this) { - case toMention: - return MentionBlockKeys.type; - case toBookmark: - return LinkPreviewBlockKeys.type; - case toEmbed: - return LinkPreviewBlockKeys.type; - } - } -} - -extension LinkExtension on BuildContext { - Future copyLink(String link) async { - if (link.isEmpty) return; - await getIt() - .setData(ClipboardServiceData(plainText: link)); - if (mounted) { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkSuccess.tr(), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart deleted file mode 100644 index d08442d779..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.dart'; -import 'package:flutter/services.dart'; - -import 'link_create_menu.dart'; -import 'link_styles.dart'; - -void showReplaceMenu({ - required BuildContext context, - required EditorState editorState, - required Node node, - String? url, - required LTRB ltrb, - required ValueChanged onReplace, -}) { - OverlayEntry? overlay; - - void dismissOverlay() { - keepEditorFocusNotifier.decrease(); - overlay?.remove(); - overlay = null; - } - - keepEditorFocusNotifier.increase(); - overlay = FullScreenOverlayEntry( - top: ltrb.top, - bottom: ltrb.bottom, - left: ltrb.left, - right: ltrb.right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) { - return LinkReplaceMenu( - link: url ?? '', - onSubmitted: (link) async { - onReplace.call(link); - dismissOverlay(); - }, - onDismiss: dismissOverlay, - ); - }, - ).build(); - - Overlay.of(context, rootOverlay: true).insert(overlay!); -} - -class LinkReplaceMenu extends StatefulWidget { - const LinkReplaceMenu({ - super.key, - required this.onSubmitted, - required this.link, - required this.onDismiss, - }); - - final ValueChanged onSubmitted; - final VoidCallback onDismiss; - final String link; - - @override - State createState() => _LinkReplaceMenuState(); -} - -class _LinkReplaceMenuState extends State { - bool showErrorText = false; - late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); - late TextEditingController textEditingController = - TextEditingController(text: widget.link); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - }); - } - - @override - void dispose() { - focusNode.dispose(); - textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: 330, - padding: EdgeInsets.all(8), - decoration: buildToolbarLinkDecoration(context), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: buildLinkField()), - HSpace(8), - buildReplaceButton(), - ], - ), - ); - } - - Widget buildLinkField() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 32, - child: TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - autofocus: true, - focusNode: focusNode, - textAlign: TextAlign.left, - controller: textEditingController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_pasteHint - .tr(), - context, - showErrorBorder: showErrorText, - ), - ), - ), - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - } - - Widget buildReplaceButton() { - return FlowyTextButton( - LocaleKeys.button_replace.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - fontWeight: FontWeight.w400, - onPressed: onSubmit, - ); - } - - void onSubmit() { - final link = textEditingController.text.trim(); - if (link.isEmpty || !isUri(link)) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onSubmitted.call(link); - } - - KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - if (key.logicalKey == LogicalKeyboardKey.escape) { - widget.onDismiss.call(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.enter) { - onSubmit(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart deleted file mode 100644 index 97fd6abdad..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/list_extension.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'link_create_menu.dart'; -import 'link_styles.dart'; - -class LinkSearchTextField { - LinkSearchTextField({ - this.onEscape, - this.onEnter, - this.onDataRefresh, - this.initialViewId = '', - required this.currentViewId, - String? initialSearchText, - }) : textEditingController = TextEditingController( - text: isUri(initialSearchText ?? '') ? initialSearchText : '', - ); - - final TextEditingController textEditingController; - final String initialViewId; - final String currentViewId; - final ItemScrollController searchController = ItemScrollController(); - late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); - final List searchedViews = []; - final List recentViews = []; - int selectedIndex = 0; - - final VoidCallback? onEscape; - final VoidCallback? onEnter; - final VoidCallback? onDataRefresh; - - String get searchText => textEditingController.text; - - bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText); - - bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty; - - ViewPB get currentSearchedView => searchedViews[selectedIndex]; - - ViewPB get currentRecentView => recentViews[selectedIndex]; - - void dispose() { - textEditingController.dispose(); - focusNode.dispose(); - searchedViews.clear(); - recentViews.clear(); - } - - Widget buildTextField({ - bool autofocus = false, - bool showError = false, - required BuildContext context, - }) { - return TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - autofocus: autofocus, - focusNode: focusNode, - textAlign: TextAlign.left, - controller: textEditingController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - onChanged: (text) { - if (text.isEmpty) { - searchedViews.clear(); - selectedIndex = 0; - onDataRefresh?.call(); - } else { - searchViews(text); - } - }, - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_toolbar_linkInputHint.tr(), - context, - showErrorBorder: showError, - ), - ); - } - - Widget buildResultContainer({ - EdgeInsetsGeometry? margin, - required BuildContext context, - VoidCallback? onLinkSelected, - ValueChanged? onPageLinkSelected, - double width = 320.0, - }) { - return onSearchResult( - onEmpty: () => SizedBox.shrink(), - onLink: () => Container( - height: 48, - width: width, - padding: EdgeInsets.all(8), - margin: margin, - decoration: buildToolbarLinkDecoration(context), - child: FlowyButton( - leftIcon: FlowySvg(FlowySvgs.toolbar_link_earth_m), - isSelected: true, - text: FlowyText.regular( - searchText, - overflow: TextOverflow.ellipsis, - fontSize: 14, - figmaLineHeight: 20, - ), - onTap: onLinkSelected, - ), - ), - onRecentViews: () => Container( - width: width, - height: recentViews.length.clamp(1, 5) * 32.0 + 48, - margin: margin, - padding: EdgeInsets.all(8), - decoration: buildToolbarLinkDecoration(context), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 32, - padding: EdgeInsets.all(8), - child: FlowyText.semibold( - LocaleKeys.inlineActions_recentPages.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Flexible( - child: ListView.builder( - itemBuilder: (context, index) { - final currentView = recentViews[index]; - return buildPageItem( - currentView, - index == selectedIndex, - onPageLinkSelected, - ); - }, - itemCount: recentViews.length, - ), - ), - ], - ), - ), - onSearchViews: () => Container( - width: width, - height: searchedViews.length.clamp(1, 5) * 32.0 + 16, - margin: margin, - decoration: buildToolbarLinkDecoration(context), - child: ScrollablePositionedList.builder( - padding: EdgeInsets.all(8), - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - itemCount: searchedViews.length, - itemScrollController: searchController, - initialScrollIndex: max(0, selectedIndex), - itemBuilder: (context, index) { - final currentView = searchedViews[index]; - return buildPageItem( - currentView, - index == selectedIndex, - onPageLinkSelected, - ); - }, - ), - ), - ); - } - - Widget buildPageItem( - ViewPB view, - bool isSelected, - ValueChanged? onSubmittedPageLink, - ) { - final viewName = view.name; - final displayName = viewName.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : viewName; - final isCurrent = initialViewId == view.id; - return SizedBox( - height: 32, - child: FlowyButton( - isSelected: isSelected, - leftIcon: buildIcon(view, padding: EdgeInsets.zero), - text: FlowyText.regular( - displayName, - overflow: TextOverflow.ellipsis, - fontSize: 14, - figmaLineHeight: 20, - ), - rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () => onSubmittedPageLink?.call(view), - ), - ); - } - - Widget buildIcon( - ViewPB view, { - EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4), - }) { - if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20)); - final iconData = view.icon.toEmojiIconData(); - return Padding( - padding: padding, - child: RawEmojiIconWidget( - emoji: iconData, - emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20, - lineHeight: 1, - ), - ); - } - - void requestFocus() => focusNode.requestFocus(); - - void unfocus() => focusNode.unfocus(); - - void updateText(String text) => textEditingController.text = text; - - T onSearchResult({ - required ValueGetter onLink, - required ValueGetter onRecentViews, - required ValueGetter onSearchViews, - required ValueGetter onEmpty, - }) { - if (searchedViews.isEmpty && recentViews.isEmpty && searchText.isEmpty) { - return onEmpty.call(); - } - if (searchedViews.isEmpty && searchText.isNotEmpty) { - return onLink.call(); - } - if (searchedViews.isEmpty) return onRecentViews.call(); - return onSearchViews.call(); - } - - KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - int index = selectedIndex; - if (key.logicalKey == LogicalKeyboardKey.escape) { - onEscape?.call(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.arrowUp) { - index = onSearchResult( - onLink: () => 0, - onRecentViews: () { - int result = index - 1; - if (result < 0) result = recentViews.length - 1; - return result; - }, - onSearchViews: () { - int result = index - 1; - if (result < 0) result = searchedViews.length - 1; - searchController.scrollTo( - index: result, - alignment: 0.5, - duration: const Duration(milliseconds: 300), - ); - return result; - }, - onEmpty: () => 0, - ); - refreshIndex(index); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.arrowDown) { - index = onSearchResult( - onLink: () => 0, - onRecentViews: () { - int result = index + 1; - if (result >= recentViews.length) result = 0; - return result; - }, - onSearchViews: () { - int result = index + 1; - if (result >= searchedViews.length) result = 0; - searchController.scrollTo( - index: result, - alignment: 0.5, - duration: const Duration(milliseconds: 300), - ); - return result; - }, - onEmpty: () => 0, - ); - refreshIndex(index); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.enter) { - onEnter?.call(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - Future searchRecentViews() async { - final recentService = getIt(); - final sectionViews = await recentService.recentViews(); - final views = sectionViews - .unique((e) => e.item.id) - .map((e) => e.item) - .where((e) => e.id != currentViewId) - .take(5) - .toList(); - recentViews.clear(); - recentViews.addAll(views); - selectedIndex = 0; - onDataRefresh?.call(); - } - - Future searchViews(String search) async { - final viewResult = await ViewBackendService.getAllViews(); - final allViews = viewResult - .toNullable() - ?.items - .where( - (view) => - (view.id != currentViewId) && - (view.name.toLowerCase().contains(search.toLowerCase()) || - (view.name.isEmpty && search.isEmpty) || - (view.name.isEmpty && - LocaleKeys.menuAppHeader_defaultNewPageName - .tr() - .toLowerCase() - .contains(search.toLowerCase()))), - ) - .take(10) - .toList(); - searchedViews.clear(); - searchedViews.addAll(allViews ?? []); - selectedIndex = 0; - onDataRefresh?.call(); - } - - void refreshIndex(int index) { - selectedIndex = index; - onDataRefresh?.call(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart deleted file mode 100644 index cabc00a312..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -class LinkStyle { - static const textTertiary = Color(0xFF99A1A8); - static const textStatusError = Color(0xffE71D32); - static const fillThemeThick = Color(0xFF00B5FF); - static const shadowMedium = Color(0x1F22251F); - static const textPrimary = Color(0xFF1F2329); - - static Color borderColor(BuildContext context) => - Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0x64BDBDBD); - - static InputDecoration buildLinkTextFieldInputDecoration( - String hintText, - BuildContext context, { - bool showErrorBorder = false, - }) { - final border = OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderSide: BorderSide(color: borderColor(context)), - ); - final enableBorder = border.copyWith( - borderSide: BorderSide( - color: showErrorBorder - ? LinkStyle.textStatusError - : LinkStyle.fillThemeThick, - ), - ); - const hintStyle = TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - color: LinkStyle.textTertiary, - ); - return InputDecoration( - hintText: hintText, - hintStyle: hintStyle, - contentPadding: const EdgeInsets.fromLTRB(8, 6, 8, 6), - isDense: true, - border: border, - enabledBorder: border, - focusedBorder: enableBorder, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart deleted file mode 100644 index 7598a2b657..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; - -class ToolbarAnimationWidget extends StatefulWidget { - const ToolbarAnimationWidget({ - super.key, - required this.child, - this.duration = const Duration(milliseconds: 150), - this.beginOpacity = 0.0, - this.endOpacity = 1.0, - this.beginScaleFactor = 0.95, - this.endScaleFactor = 1.0, - }); - - final Widget child; - final Duration duration; - final double beginScaleFactor; - final double endScaleFactor; - final double beginOpacity; - final double endOpacity; - - @override - State createState() => _ToolbarAnimationWidgetState(); -} - -class _ToolbarAnimationWidgetState extends State - with SingleTickerProviderStateMixin { - late AnimationController controller; - late Animation fadeAnimation; - late Animation scaleAnimation; - - @override - void initState() { - super.initState(); - controller = AnimationController( - vsync: this, - duration: widget.duration, - ); - fadeAnimation = _buildFadeAnimation(); - scaleAnimation = _buildScaleAnimation(); - controller.forward(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: controller, - builder: (_, child) => Opacity( - opacity: fadeAnimation.value, - child: Transform.scale( - scale: scaleAnimation.value, - child: child, - ), - ), - child: widget.child, - ); - } - - Animation _buildFadeAnimation() { - return Tween( - begin: widget.beginOpacity, - end: widget.endOpacity, - ).animate( - CurvedAnimation( - parent: controller, - curve: Curves.easeInOut, - ), - ); - } - - Animation _buildScaleAnimation() { - return Tween( - begin: widget.beginScaleFactor, - end: widget.endScaleFactor, - ).animate( - CurvedAnimation( - parent: controller, - curve: Curves.easeInOut, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 6c09ca6a28..a37ed29150 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -30,10 +30,6 @@ class ErrorBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -47,7 +43,6 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -86,7 +81,6 @@ class _ErrorBlockComponentWidgetState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -154,6 +148,7 @@ class _ErrorBlockComponentWidgetState extends State void _copyBlockContent() { showToastNotification( + context, message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart index fe50224caa..52fc2e717f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart @@ -10,7 +10,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; @@ -96,17 +95,6 @@ enum FileUrlType { return 2; } } - - FileUploadTypePB toFileUploadTypePB() { - switch (this) { - case FileUrlType.local: - return FileUploadTypePB.LocalFile; - case FileUrlType.network: - return FileUploadTypePB.NetworkFile; - case FileUrlType.cloud: - return FileUploadTypePB.CloudFile; - } - } } Node fileNode({ @@ -153,7 +141,6 @@ class FileBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -316,7 +303,6 @@ class FileBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart index 99529b3b8e..d79d5a1994 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart @@ -1,17 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FileBlockMenu extends StatefulWidget { @@ -61,37 +59,11 @@ class _FileBlockMenuState extends State { final dateFormat = context.read().state.dateFormat; final urlType = FileUrlType.fromIntValue(widget.node.attributes[FileBlockKeys.urlType]); - final fileUploadType = urlType.toFileUploadTypePB(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - HoverButton( - itemHeight: 20, - leftIcon: const FlowySvg(FlowySvgs.download_s), - name: LocaleKeys.button_download.tr(), - onTap: () { - final userProfile = widget.editorState.document.root.context - ?.read() - .state - .userProfilePB; - final url = widget.node.attributes[FileBlockKeys.url]; - final name = widget.node.attributes[FileBlockKeys.name]; - if (url != null && name != null) { - final filePB = MediaFilePB( - url: url, - name: name, - uploadType: fileUploadType, - ); - downloadMediaFile( - context, - filePB, - userProfile: userProfile, - ); - } - }, - ), - const VSpace(4), HoverButton( itemHeight: 20, leftIcon: const FlowySvg(FlowySvgs.edit_s), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart index 8e6651ff73..3ab93b4c95 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart @@ -14,8 +14,8 @@ import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:cross_file/cross_file.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_impl.dart'; @@ -104,10 +104,10 @@ Future downloadMediaFile( await afLaunchUrlString(file.url); } else { if (userProfile == null) { - showToastNotification( + return showToastNotification( + context, message: LocaleKeys.grid_media_downloadFailedToken.tr(), ); - return; } final uri = Uri.parse(file.url); @@ -128,12 +128,14 @@ Future downloadMediaFile( if (result != null && context.mounted) { showToastNotification( + context, type: ToastificationType.error, message: LocaleKeys.grid_media_downloadSuccess.tr(), ); } } else if (context.mounted) { showToastNotification( + context, type: ToastificationType.error, message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), ); @@ -157,11 +159,13 @@ Future downloadMediaFile( if (context.mounted) { showToastNotification( + context, message: LocaleKeys.grid_media_downloadSuccess.tr(), ); } } else if (context.mounted) { showToastNotification( + context, type: ToastificationType.error, message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), ); @@ -184,8 +188,8 @@ Future insertLocalFile( final fileType = file.fileType.toMediaFileTypePB(); // Check upload type - final isLocalMode = - (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; + final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == + AuthenticatorPB.Local; String? path; String? errorMsg; @@ -229,8 +233,8 @@ Future insertLocalFiles( if (files.every((f) => f.path.isEmpty)) return; // Check upload type - final isLocalMode = - (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; + final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == + AuthenticatorPB.Local; for (final file in files) { final fileType = file.fileType.toMediaFileTypePB(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart index e0f63e57c7..d297328681 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart @@ -9,6 +9,8 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -19,6 +21,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; +const kFontToolbarItemId = 'editor.font'; + +@visibleForTesting +const kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem'); + +final customizeFontToolbarItem = ToolbarItem( + id: kFontToolbarItemId, + group: 4, + isActive: onlyShowInTextType, + builder: (context, editorState, highlightColor, _, tooltipBuilder) { + final selection = editorState.selection!; + final popoverController = PopoverController(); + final String? currentFontFamily = editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); + + Widget child = FontFamilyDropDown( + currentFontFamily: currentFontFamily ?? '', + offset: const Offset(0, 12), + popoverController: popoverController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + onFontFamilyChanged: (fontFamily) async { + popoverController.close(); + try { + await editorState.formatDelta(selection, { + AppFlowyRichTextKeys.fontFamily: fontFamily, + }); + } catch (e) { + Log.error('Failed to set font family: $e'); + } + }, + onResetFont: () async { + popoverController.close(); + await editorState + .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); + }, + child: FlowyButton( + key: kFontFamilyToolbarItemKey, + useIntrinsicWidth: true, + hoverColor: Colors.grey.withValues(alpha: 0.3), + onTap: () => popoverController.show(), + text: const FlowySvg( + FlowySvgs.font_family_s, + size: Size.square(16.0), + color: Colors.white, + ), + ), + ); + + if (tooltipBuilder != null) { + child = tooltipBuilder( + context, + kFontToolbarItemId, + LocaleKeys.document_plugins_fonts.tr(), + child, + ); + } + + return child; + }, +); + class ThemeFontFamilySetting extends StatefulWidget { const ThemeFontFamilySetting({ super.key, @@ -99,11 +163,6 @@ class _FontFamilyDropDownState extends State { popoverKey: ThemeFontFamilySetting.popoverKey, popoverController: widget.popoverController, currentValue: currentValue, - margin: EdgeInsets.zero, - boxConstraints: const BoxConstraints( - maxWidth: 240, - maxHeight: 420, - ), onClose: () { query.value = ''; widget.onClose?.call(); @@ -112,25 +171,27 @@ class _FontFamilyDropDownState extends State { child: widget.child, popupBuilder: (_) { widget.onOpen?.call(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: FlowyTextField( - key: ThemeFontFamilySetting.textFieldKey, - hintText: LocaleKeys.settings_appearance_fontFamily_search.tr(), - autoFocus: false, - debounceDuration: const Duration(milliseconds: 300), - onChanged: (value) { - setState(() { + return CustomScrollView( + shrinkWrap: true, + slivers: [ + SliverPadding( + padding: const EdgeInsets.only(right: 8), + sliver: SliverToBoxAdapter( + child: FlowyTextField( + key: ThemeFontFamilySetting.textFieldKey, + hintText: + LocaleKeys.settings_appearance_fontFamily_search.tr(), + autoFocus: false, + debounceDuration: const Duration(milliseconds: 300), + onChanged: (value) { query.value = value; - }); - }, + }, + ), ), ), - Container(height: 1, color: Theme.of(context).dividerColor), + const SliverToBoxAdapter( + child: SizedBox(height: 4), + ), ValueListenableBuilder( valueListenable: query, builder: (context, value, child) { @@ -145,32 +206,14 @@ class _FontFamilyDropDownState extends State { .sorted((a, b) => levenshtein(a, b)) .toList(); } - return displayed.length >= 10 - ? Flexible( - child: ListView.builder( - padding: const EdgeInsets.all(8.0), - itemBuilder: (context, index) => - _fontFamilyItemButton( - context, - getGoogleFontSafely(displayed[index]), - ), - itemCount: displayed.length, - ), - ) - : Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: List.generate( - displayed.length, - (index) => _fontFamilyItemButton( - context, - getGoogleFontSafely(displayed[index]), - ), - ), - ), - ); + return SliverFixedExtentList.builder( + itemBuilder: (context, index) => _fontFamilyItemButton( + context, + getGoogleFontSafely(displayed[index]), + ), + itemCount: displayed.length, + itemExtent: 32, + ); }, ), ], @@ -190,18 +233,16 @@ class _FontFamilyDropDownState extends State { waitDuration: const Duration(milliseconds: 150), child: SizedBox( key: ValueKey(buttonFontFamily), - height: 36, + height: 32, child: FlowyButton( onHover: (_) => FocusScope.of(context).unfocus(), - text: FlowyText( + text: FlowyText.medium( buttonFontFamily.fontFamilyDisplayName, fontFamily: buttonFontFamily, - figmaLineHeight: 20, - fontWeight: FontWeight.w400, ), rightIcon: buttonFontFamily == widget.currentFontFamily.parseFontFamilyName() - ? const FlowySvg(FlowySvgs.toolbar_check_m) + ? const FlowySvg(FlowySvgs.check_s) : null, onTap: () { if (widget.onFontFamilyChanged != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 16605367ca..c4f0491596 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -188,65 +188,49 @@ class _DocumentCoverWidgetState extends State { onChangeCover: (type, details) => _saveIconOrCover(cover: (type, details)), ), - _buildAlignedCoverIcon(context), + _buildCoverIcon( + context, + constraints, + offset, + ), ], ), - _buildAlignedTitle(context), + Padding( + padding: EdgeInsets.fromLTRB(offset, 0, offset, 12), + child: MouseRegion( + onEnter: (event) => isCoverTitleHovered.value = true, + onExit: (event) => isCoverTitleHovered.value = false, + child: CoverTitle( + view: widget.view, + ), + ), + ), ], ); }, ); } - Widget _buildAlignedTitle(BuildContext context) { - return Center( - child: Container( - constraints: BoxConstraints( - maxWidth: widget.editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: widget.editorState.editorStyle.padding + - const EdgeInsets.symmetric(horizontal: 44), - child: MouseRegion( - onEnter: (event) => isCoverTitleHovered.value = true, - onExit: (event) => isCoverTitleHovered.value = false, - child: CoverTitle( - view: widget.view, - ), - ), - ), - ); - } - - Widget _buildAlignedCoverIcon(BuildContext context) { - if (!hasIcon) { + Widget _buildCoverIcon( + BuildContext context, + BoxConstraints constraints, + double offset, + ) { + if (!hasIcon || offset == 0) { return const SizedBox.shrink(); } return Positioned( + // if hasCover, there shouldn't be icons present so the icon can + // be closer to the bottom. + left: offset, bottom: hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight, - left: 0, - right: 0, - child: Center( - child: Container( - constraints: BoxConstraints( - maxWidth: - widget.editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: widget.editorState.editorStyle.padding + - const EdgeInsets.symmetric(horizontal: 44), - child: Row( - children: [ - DocumentIcon( - editorState: widget.editorState, - node: widget.node, - icon: viewIcon, - documentId: view.id, - onChangeIcon: (icon) => _saveIconOrCover(icon: icon), - ), - Spacer(), - ], - ), - ), + child: DocumentIcon( + editorState: widget.editorState, + node: widget.node, + icon: viewIcon, + documentId: view.id, + onChangeIcon: (icon) => _saveIconOrCover(icon: icon), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index cda76233d6..99b50c49c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -8,7 +8,6 @@ import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; import 'package:string_validator/string_validator.dart'; @@ -72,13 +71,11 @@ class RawEmojiIconWidget extends StatefulWidget { required this.emoji, required this.emojiSize, this.enableColor = true, - this.lineHeight, }); final EmojiIconData emoji; final double emojiSize; final bool enableColor; - final double? lineHeight; @override State createState() => _RawEmojiIconWidgetState(); @@ -114,21 +111,26 @@ class _RawEmojiIconWidgetState extends State { try { switch (widget.emoji.type) { case FlowyIconType.emoji: - return FlowyText.emoji( - widget.emoji.emoji, - fontSize: widget.emojiSize, - textAlign: TextAlign.justify, - lineHeight: widget.lineHeight, + return SizedBox( + width: widget.emojiSize, + child: EmojiText( + emoji: widget.emoji.emoji, + fontSize: widget.emojiSize, + textAlign: TextAlign.justify, + ), ); case FlowyIconType.icon: - IconsData iconData = IconsData.fromJson( - jsonDecode(widget.emoji.emoji), - ); + IconsData iconData = + IconsData.fromJson(jsonDecode(widget.emoji.emoji)); if (!widget.enableColor) { iconData = iconData.noColor(); } - final iconSize = widget.emojiSize; + /// Under the same width conditions, icons on macOS seem to appear + /// larger than emojis, so 0.9 is used here to slightly reduce the + /// size of the icons + final iconSize = + Platform.isMacOS ? widget.emojiSize * 0.9 : widget.emojiSize; return IconWidget( iconsData: iconData, size: iconSize, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index 7f0105134d..0471358464 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -123,7 +123,6 @@ class CustomImageBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.showMenu = false, this.menuBuilder, @@ -227,7 +226,6 @@ class CustomImageBlockComponentState extends State delegate: this, listenable: editorState.selectionNotifier, blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, supportTypes: const [BlockSelectionType.block], child: child, ); @@ -237,7 +235,6 @@ class CustomImageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -314,7 +311,7 @@ class CustomImageBlockComponentState extends State }) { final imageBox = imageKey.currentContext?.findRenderObject(); if (imageBox is RenderBox) { - return padding.topLeft & imageBox.size; + return Offset.zero & imageBox.size; } return Rect.zero; } @@ -378,6 +375,7 @@ class CustomImageBlockComponentState extends State onTap: () async { context.pop(); showToastNotification( + context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); await getIt().setPlainText(url); @@ -430,6 +428,7 @@ class CustomImageBlockComponentState extends State ); if (mounted) { showToastNotification( + context, message: result.isSuccess ? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr() : LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index d11d943066..4a6260d8b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -117,12 +117,14 @@ class _ImageMenuState extends State { if (mounted) { showToastNotification( + context, message: LocaleKeys.message_copy_success.tr(), ); } } catch (e) { if (mounted) { showToastNotification( + context, message: LocaleKeys.message_copy_fail.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart index 51da975938..88f60db494 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart @@ -82,7 +82,6 @@ class MultiImageBlockComponent extends BlockComponentStatefulWidget { this.menuBuilder, super.configuration = const BlockComponentConfiguration(), super.actionBuilder, - super.actionTrailingBuilder, }); final bool showMenu; @@ -191,7 +190,6 @@ class MultiImageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart index dc95054e81..b98e05f231 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart @@ -218,6 +218,7 @@ class _MultiImageMenuState extends State { ClipboardData(text: images[widget.indexNotifier.value].url), ); showToastNotification( + context, message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart index cd3779cb6c..061a6fe320 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart @@ -9,7 +9,7 @@ const _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation'; final ToolbarItem inlineMathEquationItem = ToolbarItem( id: _kInlineMathEquationToolbarItemId, - group: 4, + group: 2, isActive: onlyShowInSingleSelectionAndTextType, builder: (context, editorState, highlightColor, _, tooltipBuilder) { final selection = editorState.selection!; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart deleted file mode 100644 index baf9702a36..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart +++ /dev/null @@ -1,310 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'link_embed_menu.dart'; - -class LinkEmbedKeys { - const LinkEmbedKeys._(); - static const String previewType = 'preview_type'; - static const String embed = 'embed'; - static const String align = 'align'; -} - -Node linkEmbedNode({required String url}) => Node( - type: LinkPreviewBlockKeys.type, - attributes: { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: LinkEmbedKeys.embed, - }, - ); - -class LinkEmbedBlockComponent extends BlockComponentStatefulWidget { - const LinkEmbedBlockComponent({ - super.key, - super.showActions, - super.actionBuilder, - super.configuration = const BlockComponentConfiguration(), - required super.node, - }); - - @override - DefaultSelectableMixinState createState() => - LinkEmbedBlockComponentState(); -} - -class LinkEmbedBlockComponentState - extends DefaultSelectableMixinState - with BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - String get url => widget.node.attributes[LinkPreviewBlockKeys.url] ?? ''; - - LinkLoadingStatus status = LinkLoadingStatus.loading; - final parser = LinkParser(); - late LinkInfo linkInfo = LinkInfo(url: url); - - final showActionsNotifier = ValueNotifier(false); - bool isMenuShowing = false, isHovering = false; - - @override - void initState() { - super.initState(); - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = LinkLoadingStatus.idle; - } else if (!hasOldInfo) { - status = LinkLoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - parser.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget result = MouseRegion( - onEnter: (_) { - isHovering = true; - showActionsNotifier.value = true; - }, - onExit: (_) { - isHovering = false; - Future.delayed(const Duration(milliseconds: 200), () { - if (isMenuShowing || isHovering) return; - if (mounted) showActionsNotifier.value = false; - }); - }, - child: buildChild(context), - ); - final parent = node.parent; - EdgeInsets newPadding = padding; - if (parent?.type == CalloutBlockKeys.type) { - newPadding = padding.copyWith(right: padding.right + 10); - } - - result = Padding(padding: newPadding, child: result); - - if (widget.showActions && widget.actionBuilder != null) { - result = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - child: result, - ); - } - return result; - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - fillSceme = theme.fillColorScheme, - borderScheme = theme.borderColorScheme; - Widget child; - final isIdle = status == LinkLoadingStatus.idle; - if (isIdle) { - child = buildContent(context); - } else { - child = buildErrorLoadingWidget(context); - } - return Container( - height: 450, - key: widgetKey, - decoration: BoxDecoration( - color: isIdle ? Theme.of(context).cardColor : fillSceme.tertiaryHover, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: Border.all(color: borderScheme.greyTertiary), - ), - child: Stack( - children: [ - child, - buildMenu(context), - ], - ), - ); - } - - Widget buildMenu(BuildContext context) { - return Positioned( - top: 12, - right: 12, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, showActions, child) { - if (!showActions) return SizedBox.shrink(); - return LinkEmbedMenu( - editorState: context.read(), - node: node, - onReload: () { - setState(() { - status = LinkLoadingStatus.loading; - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) parser.start(url); - }); - }, - onMenuShowed: () { - isMenuShowing = true; - }, - onMenuHided: () { - isMenuShowing = false; - if (!isHovering && mounted) { - showActionsNotifier.value = false; - } - }, - ); - }, - ), - ); - } - - Widget buildContent(BuildContext context) { - final theme = AppFlowyTheme.of(context), textScheme = theme.textColorScheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - child: FlowyNetworkImage( - url: linkInfo.imageUrl ?? '', - width: MediaQuery.of(context).size.width, - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => - afLaunchUrlString(url, addingHttpSchemeWhenFailed: true), - child: Container( - height: 64, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - child: Row( - children: [ - SizedBox.square( - dimension: 40, - child: Center( - child: linkInfo.buildIconWidget(size: Size.square(32)), - ), - ), - HSpace(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - linkInfo.siteName ?? '', - color: textScheme.primary, - fontSize: 14, - figmaLineHeight: 20, - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - ), - VSpace(4), - FlowyText.regular( - url, - color: textScheme.secondary, - fontSize: 12, - figmaLineHeight: 16, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget buildErrorLoadingWidget(BuildContext context) { - final theme = AppFlowyTheme.of(context), textSceme = theme.textColorScheme; - final isLoading = status == LinkLoadingStatus.loading; - return isLoading - ? Center( - child: SizedBox.square( - dimension: 64, - child: CircularProgressIndicator.adaptive(), - ), - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - FlowySvgs.embed_error_xl.path, - ), - VSpace(4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: RichText( - maxLines: 1, - overflow: TextOverflow.ellipsis, - text: TextSpan( - children: [ - TextSpan( - text: '$url ', - style: TextStyle( - color: textSceme.primary, - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w700, - ), - ), - TextSpan( - text: LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_unableToDisplay - .tr(), - style: TextStyle( - color: textSceme.primary, - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - - @override - Node get currentNode => node; - - @override - EdgeInsets get boxPadding => padding; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart deleted file mode 100644 index c3d2aebbcc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart +++ /dev/null @@ -1,354 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -import 'link_embed_block_component.dart'; - -class LinkEmbedMenu extends StatefulWidget { - const LinkEmbedMenu({ - super.key, - required this.node, - required this.editorState, - required this.onMenuShowed, - required this.onMenuHided, - required this.onReload, - }); - - final Node node; - final EditorState editorState; - final VoidCallback onMenuShowed; - final VoidCallback onMenuHided; - final VoidCallback onReload; - - @override - State createState() => _LinkEmbedMenuState(); -} - -class _LinkEmbedMenuState extends State { - final turnintoController = PopoverController(); - final moreOptionController = PopoverController(); - int turnintoMenuNum = 0, moreOptionNum = 0, alignMenuNum = 0; - final moreOptionButtonKey = GlobalKey(); - bool get isTurnIntoShowing => turnintoMenuNum > 0; - bool get isMoreOptionShowing => moreOptionNum > 0; - bool get isAlignMenuShowing => alignMenuNum > 0; - - Node get node => widget.node; - EditorState get editorState => widget.editorState; - - String get url => node.attributes[LinkPreviewBlockKeys.url] ?? ''; - - @override - void dispose() { - super.dispose(); - turnintoController.close(); - moreOptionController.close(); - widget.onMenuHided.call(); - } - - @override - Widget build(BuildContext context) { - return buildChild(); - } - - Widget buildChild() { - final theme = AppFlowyTheme.of(context), - iconScheme = theme.iconColorScheme, - fillScheme = theme.fillColorScheme; - - return Container( - padding: EdgeInsets.all(4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: fillScheme.primaryAlpha80, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // FlowyIconButton( - // icon: FlowySvg( - // FlowySvgs.embed_fullscreen_m, - // color: iconScheme.tertiary, - // ), - // tooltipText: LocaleKeys.document_imageBlock_openFullScreen.tr(), - // preferBelow: false, - // onPressed: () {}, - // ), - FlowyIconButton( - icon: FlowySvg( - FlowySvgs.toolbar_link_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - onPressed: () => copyLink(context), - ), - buildconvertBotton(), - buildMoreOptionBotton(), - ], - ), - ); - } - - Widget buildconvertBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; - return AppFlowyPopover( - offset: Offset(0, 6), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: turnintoController, - onOpen: () { - keepEditorFocusNotifier.increase(); - turnintoMenuNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - turnintoMenuNum--; - checkToHideMenu(); - }, - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg( - FlowySvgs.turninto_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - onPressed: showTurnIntoMenu, - ), - ); - } - - Widget buildConvertMenu() { - final types = LinkEmbedConvertCommand.values; - return Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(types.length, (index) { - final command = types[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () { - if (command == LinkEmbedConvertCommand.toBookmark) { - final transaction = editorState.transaction; - transaction.updateNode(node, { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: '', - }); - editorState.apply(transaction); - } else if (command == LinkEmbedConvertCommand.toMention) { - convertUrlPreviewNodeToMention(editorState, node); - } else if (command == LinkEmbedConvertCommand.toURL) { - convertUrlPreviewNodeToLink(editorState, node); - } - }, - ), - ); - }), - ), - ); - } - - Widget buildMoreOptionBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; - return AppFlowyPopover( - offset: Offset(0, 6), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: moreOptionController, - onOpen: () { - keepEditorFocusNotifier.increase(); - moreOptionNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - moreOptionNum--; - checkToHideMenu(); - }, - popupBuilder: (context) => buildMoreOptionMenu(), - child: FlowyIconButton( - key: moreOptionButtonKey, - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.document_toolbar_moreOptions.tr(), - preferBelow: false, - onPressed: showMoreOptionMenu, - ), - ); - } - - Widget buildMoreOptionMenu() { - final types = LinkEmbedMenuCommand.values; - return Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(types.length, (index) { - final command = types[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onEmbedMenuCommand(command), - ), - ); - }), - ), - ); - } - - void showTurnIntoMenu() { - keepEditorFocusNotifier.increase(); - turnintoController.show(); - checkToShowMenu(); - turnintoMenuNum++; - if (isMoreOptionShowing) closeMoreOptionMenu(); - } - - void closeTurnIntoMenu() { - turnintoController.close(); - checkToHideMenu(); - } - - void showMoreOptionMenu() { - keepEditorFocusNotifier.increase(); - moreOptionController.show(); - checkToShowMenu(); - moreOptionNum++; - if (isTurnIntoShowing) closeTurnIntoMenu(); - } - - void closeMoreOptionMenu() { - moreOptionController.close(); - checkToHideMenu(); - } - - void checkToHideMenu() { - Future.delayed(Duration(milliseconds: 200), () { - if (!mounted) return; - if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { - widget.onMenuHided.call(); - } - }); - } - - void checkToShowMenu() { - if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { - widget.onMenuShowed.call(); - } - } - - Future copyLink(BuildContext context) async { - await context.copyLink(url); - widget.onMenuHided.call(); - } - - void onEmbedMenuCommand(LinkEmbedMenuCommand command) { - switch (command) { - case LinkEmbedMenuCommand.openLink: - afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - break; - case LinkEmbedMenuCommand.replace: - final box = moreOptionButtonKey.currentContext?.findRenderObject() - as RenderBox?; - if (box == null) return; - final p = box.localToGlobal(Offset.zero); - showReplaceMenu( - context: context, - editorState: editorState, - node: node, - url: url, - ltrb: LTRB(left: p.dx - 330, top: p.dy), - onReplace: (url) async { - await convertLinkBlockToOtherLinkBlock( - editorState, - node, - node.type, - url: url, - ); - }, - ); - break; - case LinkEmbedMenuCommand.reload: - widget.onReload.call(); - break; - case LinkEmbedMenuCommand.removeLink: - removeUrlPreviewLink(editorState, node); - break; - } - closeMoreOptionMenu(); - } -} - -enum LinkEmbedMenuCommand { - openLink, - replace, - reload, - removeLink; - - String get title { - switch (this) { - case openLink: - return LocaleKeys.editor_openLink.tr(); - case replace: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace - .tr(); - case reload: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} - -enum LinkEmbedConvertCommand { - toMention, - toURL, - toBookmark; - - String get title { - switch (this) { - case toMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart deleted file mode 100644 index 1907f68d29..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:convert'; -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/appflowy_network_svg.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/material.dart'; -import 'link_parsers/default_parser.dart'; -import 'link_parsers/youtube_parser.dart'; - -class LinkParser { - final Set> _listeners = >{}; - static final Map _hostToParsers = { - 'www.youtube.com': YoutubeParser(), - 'youtube.com': YoutubeParser(), - 'youtu.be': YoutubeParser(), - }; - - Future start(String url, {LinkInfoParser? parser}) async { - final uri = Uri.tryParse(LinkInfoParser.formatUrl(url)) ?? Uri.parse(url); - final data = await LinkInfoCache.get(uri); - if (data != null) { - refreshLinkInfo(data); - } - - final host = uri.host; - final currentParser = parser ?? _hostToParsers[host] ?? DefaultParser(); - await _getLinkInfo(uri, currentParser); - } - - Future _getLinkInfo(Uri uri, LinkInfoParser parser) async { - try { - final linkInfo = await parser.parse(uri) ?? LinkInfo(url: '$uri'); - if (!linkInfo.isEmpty()) await LinkInfoCache.set(uri, linkInfo); - refreshLinkInfo(linkInfo); - return linkInfo; - } catch (e, s) { - Log.error('get link info error: ', e, s); - refreshLinkInfo(LinkInfo(url: '$uri')); - return null; - } - } - - void refreshLinkInfo(LinkInfo info) { - for (final listener in _listeners) { - listener(info); - } - } - - void addLinkInfoListener(ValueChanged listener) { - _listeners.add(listener); - } - - void dispose() { - _listeners.clear(); - } -} - -class LinkInfo { - factory LinkInfo.fromJson(Map json) => LinkInfo( - siteName: json['siteName'], - url: json['url'] ?? '', - title: json['title'], - description: json['description'], - imageUrl: json['imageUrl'], - faviconUrl: json['faviconUrl'], - ); - - LinkInfo({ - required this.url, - this.siteName, - this.title, - this.description, - this.imageUrl, - this.faviconUrl, - }); - - final String url; - final String? siteName; - final String? title; - final String? description; - final String? imageUrl; - final String? faviconUrl; - - Map toJson() => { - 'url': url, - 'siteName': siteName, - 'title': title, - 'description': description, - 'imageUrl': imageUrl, - 'faviconUrl': faviconUrl, - }; - - @override - String toString() { - return 'LinkInfo{url: $url, siteName: $siteName, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl}'; - } - - bool isEmpty() { - return title == null; - } - - Widget buildIconWidget({Size size = const Size.square(20.0)}) { - final iconUrl = faviconUrl; - if (iconUrl == null) { - return FlowySvg(FlowySvgs.toolbar_link_earth_m, size: size); - } - if (iconUrl.endsWith('.svg')) { - return FlowyNetworkSvg( - iconUrl, - height: size.height, - width: size.width, - errorWidget: const FlowySvg(FlowySvgs.toolbar_link_earth_m), - ); - } - return FlowyNetworkImage( - url: iconUrl, - fit: BoxFit.contain, - height: size.height, - width: size.width, - errorWidgetBuilder: (context, error, stackTrace) => - const FlowySvg(FlowySvgs.toolbar_link_earth_m), - ); - } -} - -class LinkInfoCache { - static const _linkInfoPrefix = 'link_info'; - - static Future get(Uri uri) async { - final option = await getIt().getWithFormat( - '$_linkInfoPrefix$uri', - (value) => LinkInfo.fromJson(jsonDecode(value)), - ); - return option; - } - - static Future set(Uri uri, LinkInfo data) async { - await getIt().set( - '$_linkInfoPrefix$uri', - jsonEncode(data.toJson()), - ); - } -} - -enum LinkLoadingStatus { - loading, - idle, - error, -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 9be73fcc0b..879a71f008 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -3,10 +3,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -14,9 +12,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:universal_platform/universal_platform.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; - -import 'custom_link_parser.dart'; class CustomLinkPreviewWidget extends StatelessWidget { const CustomLinkPreviewWidget({ @@ -26,8 +21,6 @@ class CustomLinkPreviewWidget extends StatelessWidget { this.title, this.description, this.imageUrl, - this.isHovering = false, - this.status = LinkLoadingStatus.loading, }); final Node node; @@ -35,14 +28,9 @@ class CustomLinkPreviewWidget extends StatelessWidget { final String? description; final String? imageUrl; final String url; - final bool isHovering; - final LinkLoadingStatus status; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context), - borderScheme = theme.borderColorScheme, - textScheme = theme.textColorScheme; final documentFontSize = context .read() .editorStyle @@ -50,67 +38,73 @@ class CustomLinkPreviewWidget extends StatelessWidget { .text .fontSize ?? 16.0; - final isInDarkCallout = node.parent?.type == CalloutBlockKeys.type && - !Theme.of(context).isLightMode; final (fontSize, width) = UniversalPlatform.isDesktopOrWeb - ? (documentFontSize, 160.0) + ? (documentFontSize, 180.0) : (documentFontSize - 2, 120.0); final Widget child = Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( border: Border.all( - color: isHovering || isInDarkCallout - ? borderScheme.greyTertiaryHover - : borderScheme.greyTertiary, + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: BorderRadius.circular( + 6.0, ), - borderRadius: BorderRadius.circular(16.0), ), - child: SizedBox( - height: 96, + child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildImage(context), + if (imageUrl != null) + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6.0), + bottomLeft: Radius.circular(6.0), + ), + child: FlowyNetworkImage( + url: imageUrl!, + width: width, + ), + ), Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 58, 12), - child: status != LinkLoadingStatus.idle - ? buildLoadingOrErrorWidget() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (title != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText.medium( - title!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize, - color: textScheme.primary, - figmaLineHeight: 20, - ), - ), - if (description != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: FlowyText( - description!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize - 4, - figmaLineHeight: 16, - color: textScheme.primary, - ), - ), - FlowyText( - url.toString(), - overflow: TextOverflow.ellipsis, - color: textScheme.secondary, - fontSize: fontSize - 4, - figmaLineHeight: 16, - ), - ], + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (title != null) + Padding( + padding: const EdgeInsets.only( + bottom: 4.0, + right: 10.0, + ), + child: FlowyText.medium( + title!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + fontSize: fontSize, + ), ), + if (description != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText( + description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + fontSize: fontSize - 4, + ), + ), + FlowyText( + url.toString(), + overflow: TextOverflow.ellipsis, + maxLines: 2, + color: Theme.of(context).hintColor, + fontSize: fontSize - 4, + ), + ], + ), ), ), ], @@ -119,12 +113,9 @@ class CustomLinkPreviewWidget extends StatelessWidget { ); if (UniversalPlatform.isDesktopOrWeb) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => afLaunchUrlString(url), - child: child, - ), + return InkWell( + onTap: () => afLaunchUrlString(url), + child: child, ); } @@ -159,59 +150,4 @@ class CustomLinkPreviewWidget extends StatelessWidget { ), ]; } - - Widget buildImage(BuildContext context) { - if (imageUrl?.isEmpty ?? true) { - return SizedBox.shrink(); - } - final theme = AppFlowyTheme.of(context), - fillScheme = theme.fillColorScheme, - iconScheme = theme.iconColorScheme; - final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; - return ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16.0), - bottomLeft: Radius.circular(16.0), - ), - child: Container( - width: width, - color: fillScheme.quaternary, - child: FlowyNetworkImage( - url: imageUrl!, - width: width, - errorWidgetBuilder: (_, __, ___) => Center( - child: FlowySvg( - FlowySvgs.toolbar_link_earth_m, - color: iconScheme.secondary, - size: Size.square(30), - ), - ), - ), - ), - ); - } - - Widget buildLoadingOrErrorWidget() { - if (status == LinkLoadingStatus.loading) { - return const Center( - child: SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ), - ); - } else if (status == LinkLoadingStatus.error) { - return const Center( - child: SizedBox( - height: 16, - width: 16, - child: Icon( - Icons.error_outline, - color: Colors.red, - ), - ), - ); - } - return SizedBox.shrink(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart deleted file mode 100644 index 3f2128db52..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; - -import 'custom_link_preview.dart'; -import 'default_selectable_mixin.dart'; -import 'link_preview_menu.dart'; - -class CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder { - CustomLinkPreviewBlockComponentBuilder({ - super.configuration, - }); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - final isEmbed = - node.attributes[LinkEmbedKeys.previewType] == LinkEmbedKeys.embed; - if (isEmbed) { - return LinkEmbedBlockComponent( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (_, state) => - actionBuilder(blockComponentContext, state), - ); - } - return CustomLinkPreviewBlockComponent( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => - (node) => node.attributes[LinkPreviewBlockKeys.url]!.isNotEmpty; -} - -class CustomLinkPreviewBlockComponent extends BlockComponentStatefulWidget { - const CustomLinkPreviewBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - DefaultSelectableMixinState createState() => - CustomLinkPreviewBlockComponentState(); -} - -class CustomLinkPreviewBlockComponentState - extends DefaultSelectableMixinState - with BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; - - final parser = LinkParser(); - LinkLoadingStatus status = LinkLoadingStatus.loading; - late LinkInfo linkInfo = LinkInfo(url: url); - - final showActionsNotifier = ValueNotifier(false); - bool isMenuShowing = false, isHovering = false; - - @override - void initState() { - super.initState(); - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = LinkLoadingStatus.idle; - } else if (!hasOldInfo) { - status = LinkLoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - parser.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) { - isHovering = true; - showActionsNotifier.value = true; - }, - onExit: (_) { - isHovering = false; - Future.delayed(const Duration(milliseconds: 200), () { - if (isMenuShowing || isHovering) return; - if (mounted) showActionsNotifier.value = false; - }); - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, showActions, child) { - return buildPreview(showActions); - }, - ), - ); - } - - Widget buildPreview(bool showActions) { - Widget child = CustomLinkPreviewWidget( - key: widgetKey, - node: node, - url: url, - isHovering: showActions, - title: linkInfo.siteName, - description: linkInfo.description, - imageUrl: linkInfo.imageUrl, - status: status, - ); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - child: child, - ); - } - - child = Stack( - children: [ - child, - if (showActions) - Positioned( - top: 12, - right: 12, - child: CustomLinkPreviewMenu( - onMenuShowed: () { - isMenuShowing = true; - }, - onMenuHided: () { - isMenuShowing = false; - if (!isHovering && mounted) { - showActionsNotifier.value = false; - } - }, - onReload: () { - setState(() { - status = LinkLoadingStatus.loading; - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) parser.start(url); - }); - }, - node: node, - ), - ), - ], - ); - - final parent = node.parent; - EdgeInsets newPadding = padding; - if (parent?.type == CalloutBlockKeys.type) { - newPadding = padding.copyWith(right: padding.right + 10); - } - child = Padding(padding: newPadding, child: child); - - return child; - } - - @override - Node get currentNode => node; - - @override - EdgeInsets get boxPadding => padding; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart deleted file mode 100644 index c894811522..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/widgets.dart'; - -abstract class DefaultSelectableMixinState - extends State with SelectableMixin { - final widgetKey = GlobalKey(); - RenderBox? get _renderBox => - widgetKey.currentContext?.findRenderObject() as RenderBox?; - - Node get currentNode; - - EdgeInsets get boxPadding => EdgeInsets.zero; - - @override - Position start() => Position(path: currentNode.path); - - @override - Position end() => Position(path: currentNode.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - final box = _renderBox; - if (box is RenderBox) { - return boxPadding.topLeft & box.size; - } - return Rect.zero; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection(Selection.collapsed(position)); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final box = widgetKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && box is RenderBox) { - return [ - box.localToGlobal(Offset.zero, ancestor: parentBox) & box.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: currentNode.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => - _renderBox!.localToGlobal(offset); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart deleted file mode 100644 index 7b52994654..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_backend/log.dart'; -// ignore: depend_on_referenced_packages -import 'package:html/parser.dart' as html_parser; -import 'package:http/http.dart' as http; -import 'dart:convert'; - -abstract class LinkInfoParser { - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }); - - static String formatUrl(String url) { - Uri? uri = Uri.tryParse(url); - if (uri == null) return url; - if (!uri.hasScheme) uri = Uri.tryParse('http://$url'); - if (uri == null) return url; - final isHome = (uri.hasEmptyPath || uri.path == '/') && !uri.hasQuery; - final homeUrl = '${uri.scheme}://${uri.host}/'; - if (isHome) return homeUrl; - return '$uri'; - } -} - -class DefaultParser implements LinkInfoParser { - @override - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }) async { - try { - final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; - final http.Response response = - await http.get(link, headers: headers).timeout(timeout); - final code = response.statusCode; - if (code != 200 && isHome) { - throw Exception('Http request error: $code'); - } - - final contentType = response.headers['content-type']; - final charset = contentType?.split('charset=').lastOrNull; - String body = ''; - if (charset == null || - charset.toLowerCase() == 'latin-1' || - charset.toLowerCase() == 'iso-8859-1') { - body = latin1.decode(response.bodyBytes); - } else { - body = utf8.decode(response.bodyBytes, allowMalformed: true); - } - - final document = html_parser.parse(body); - - final siteName = document - .querySelector('meta[property="og:site_name"]') - ?.attributes['content']; - - String? title = document - .querySelector('meta[property="og:title"]') - ?.attributes['content']; - title ??= document.querySelector('title')?.text; - - String? description = document - .querySelector('meta[property="og:description"]') - ?.attributes['content']; - description ??= document - .querySelector('meta[name="description"]') - ?.attributes['content']; - - String? imageUrl = document - .querySelector('meta[property="og:image"]') - ?.attributes['content']; - if (imageUrl != null && !imageUrl.startsWith('http')) { - imageUrl = link.resolve(imageUrl).toString(); - } - - final favicon = - 'https://www.faviconextractor.com/favicon/${link.host}?larger=true'; - - return LinkInfo( - url: '$link', - siteName: siteName, - title: title, - description: description, - imageUrl: imageUrl, - faviconUrl: favicon, - ); - } catch (e) { - Log.error('Parse link $link error: $e'); - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart deleted file mode 100644 index 6f1ac6fb22..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:http/http.dart' as http; -import 'default_parser.dart'; - -class YoutubeParser implements LinkInfoParser { - @override - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }) async { - try { - final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; - if (isHome) { - return DefaultParser().parse( - link, - timeout: timeout, - headers: headers, - ); - } - - final requestLink = - 'https://www.youtube.com/oembed?url=$link&format=json'; - final http.Response response = await http - .get(Uri.parse(requestLink), headers: headers) - .timeout(timeout); - final code = response.statusCode; - if (code != 200) { - throw Exception('Http request error: $code'); - } - - final youtubeInfo = YoutubeInfo.fromJson(jsonDecode(response.body)); - - final favicon = - 'https://www.google.com/s2/favicons?sz=64&domain=${link.host}'; - return LinkInfo( - url: '$link', - title: youtubeInfo.title, - siteName: youtubeInfo.authorName, - imageUrl: youtubeInfo.thumbnailUrl, - faviconUrl: favicon, - ); - } catch (e) { - Log.error('Parse link $link error: $e'); - return null; - } - } -} - -class YoutubeInfo { - YoutubeInfo({ - this.title, - this.authorName, - this.version, - this.providerName, - this.providerUrl, - this.thumbnailUrl, - }); - - YoutubeInfo.fromJson(Map json) { - title = json['title']; - authorName = json['author_name']; - version = json['version']; - providerName = json['provider_name']; - providerUrl = json['provider_url']; - thumbnailUrl = json['thumbnail_url']; - } - String? title; - String? authorName; - String? version; - String? providerName; - String? providerUrl; - String? thumbnailUrl; - - Map toJson() => { - 'title': title, - 'author_name': authorName, - 'version': version, - 'provider_name': providerName, - 'provider_url': providerUrl, - 'thumbnail_url': thumbnailUrl, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart new file mode 100644 index 0000000000..6688cfe304 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; + +class LinkPreviewDataCache implements LinkPreviewDataCacheInterface { + @override + Future get(String url) async { + final option = + await getIt().getWithFormat( + url, + (value) => LinkPreviewData.fromJson(jsonDecode(value)), + ); + return option; + } + + @override + Future set(String url, LinkPreviewData data) async { + await getIt().set( + url, + jsonEncode(data.toJson()), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index 2fb493dda3..cf7d72cc2a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -1,207 +1,110 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -class CustomLinkPreviewMenu extends StatefulWidget { - const CustomLinkPreviewMenu({ +import '../image/custom_image_block_component/custom_image_block_component.dart'; + +class LinkPreviewMenu extends StatefulWidget { + const LinkPreviewMenu({ super.key, - required this.onMenuShowed, - required this.onMenuHided, - required this.onReload, required this.node, + required this.state, }); - final VoidCallback onMenuShowed; - final VoidCallback onMenuHided; - final VoidCallback onReload; + final Node node; + final LinkPreviewBlockComponentState state; @override - State createState() => _CustomLinkPreviewMenuState(); + State createState() => _LinkPreviewMenuState(); } -class _CustomLinkPreviewMenuState extends State { - final popoverController = PopoverController(); - final buttonKey = GlobalKey(); - bool closed = false; - bool selected = false; - +class _LinkPreviewMenuState extends State { @override - void dispose() { - super.dispose(); - popoverController.close(); - widget.onMenuHided.call(); + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + height: 32, + decoration: BoxDecoration( + color: theme.cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withValues(alpha: 0.1), + ), + ], + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + children: [ + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(), + iconData: FlowySvgs.m_toolbar_link_m, + onTap: () async => convertUrlPreviewNodeToLink( + context.read(), + widget.node, + ), + ), + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.editor_copyLink.tr(), + iconData: FlowySvgs.copy_s, + onTap: copyImageLink, + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.button_delete.tr(), + iconData: FlowySvgs.trash_s, + onTap: deleteLinkPreviewNode, + ), + const HSpace(4), + ], + ), + ); } + void copyImageLink() { + final url = widget.node.attributes[CustomImageBlockKeys.url]; + if (url != null) { + Clipboard.setData(ClipboardData(text: url)); + showToastNotification( + context, + message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(), + ); + } + } + + Future deleteLinkPreviewNode() async { + final node = widget.node; + final editorState = context.read(); + final transaction = editorState.transaction; + transaction.deleteNode(node); + transaction.afterSelection = null; + await editorState.apply(transaction); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + @override Widget build(BuildContext context) { - return AppFlowyPopover( - offset: Offset(0, 0.0), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - keepEditorFocusNotifier.decrease(); - if (!closed) { - closed = true; - return; - } else { - closed = false; - widget.onMenuHided.call(); - } - setState(() { - selected = false; - }); - }, - popupBuilder: (context) => buildMenu(), - child: FlowyIconButton( - key: buttonKey, - isSelected: selected, - icon: FlowySvg(FlowySvgs.toolbar_more_m), - onPressed: showPopover, + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 1, + color: Colors.grey, ), ); } - - Widget buildMenu() { - return MouseRegion( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(LinkPreviewMenuCommand.values.length, (index) { - final command = LinkPreviewMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - Future onTap(LinkPreviewMenuCommand command) async { - final editorState = context.read(); - final node = widget.node; - final url = node.attributes[LinkPreviewBlockKeys.url]; - switch (command) { - case LinkPreviewMenuCommand.convertToMention: - await convertUrlPreviewNodeToMention(editorState, node); - break; - case LinkPreviewMenuCommand.convertToUrl: - await convertUrlPreviewNodeToLink(editorState, node); - break; - case LinkPreviewMenuCommand.convertToEmbed: - final transaction = editorState.transaction; - transaction.updateNode(node, { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: LinkEmbedKeys.embed, - }); - await editorState.apply(transaction); - break; - case LinkPreviewMenuCommand.copyLink: - if (url != null) { - await context.copyLink(url); - } - break; - case LinkPreviewMenuCommand.replace: - final box = buttonKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; - final p = box.localToGlobal(Offset.zero); - showReplaceMenu( - context: context, - editorState: editorState, - node: node, - url: url, - ltrb: LTRB(left: p.dx - 330, top: p.dy), - onReplace: (url) async { - await convertLinkBlockToOtherLinkBlock( - editorState, - node, - node.type, - url: url, - ); - }, - ); - break; - case LinkPreviewMenuCommand.reload: - widget.onReload.call(); - break; - case LinkPreviewMenuCommand.removeLink: - await removeUrlPreviewLink(editorState, node); - break; - } - closePopover(); - } - - void showPopover() { - widget.onMenuShowed.call(); - keepEditorFocusNotifier.increase(); - popoverController.show(); - setState(() { - selected = true; - }); - } - - void closePopover() { - popoverController.close(); - widget.onMenuHided.call(); - } -} - -enum LinkPreviewMenuCommand { - convertToMention, - convertToUrl, - convertToEmbed, - copyLink, - replace, - reload, - removeLink; - - String get title { - switch (this) { - case convertToMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case LinkPreviewMenuCommand.convertToUrl: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case LinkPreviewMenuCommand.convertToEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case LinkPreviewMenuCommand.copyLink: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink - .tr(); - case LinkPreviewMenuCommand.replace: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace - .tr(); - case LinkPreviewMenuCommand.reload: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload - .tr(); - case LinkPreviewMenuCommand.removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart deleted file mode 100644 index fb51cdcf47..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -const _menuHeighgt = 188.0, _menuWidth = 288.0; - -class PasteAsMenuService { - PasteAsMenuService({ - required this.context, - required this.editorState, - }); - - final BuildContext context; - final EditorState editorState; - OverlayEntry? _menuEntry; - - void show(String href) { - WidgetsBinding.instance.addPostFrameCallback((_) => _show(href)); - } - - void dismiss() { - if (_menuEntry != null) { - keepEditorFocusNotifier.decrease(); - // editorState.service.scrollService?.enable(); - // editorState.service.keyboardService?.enable(); - } - _menuEntry?.remove(); - _menuEntry = null; - } - - void _show(String href) { - final Size editorSize = editorState.renderBox?.size ?? Size.zero; - if (editorSize == Size.zero) return; - final menuPosition = editorState.calculateMenuOffset( - menuWidth: _menuWidth, - menuHeight: _menuHeighgt, - ); - if (menuPosition == null) return; - final ltrb = menuPosition.ltrb; - - _menuEntry = OverlayEntry( - builder: (context) => SizedBox( - height: editorSize.height, - width: editorSize.width, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - ltrb.buildPositioned( - child: PasteAsMenu( - editorState: editorState, - onSelect: (t) { - final selection = editorState.selection; - if (selection == null) return; - final end = selection.end; - final urlSelection = Selection( - start: end.copyWith(offset: end.offset - href.length), - end: end, - ); - if (t == PasteMenuType.bookmark) { - convertUrlToLinkPreview(editorState, urlSelection, href); - } else if (t == PasteMenuType.mention) { - convertUrlToMention(editorState, urlSelection); - } else if (t == PasteMenuType.embed) { - convertUrlToLinkPreview( - editorState, - urlSelection, - href, - previewType: LinkEmbedKeys.embed, - ); - } - dismiss(); - }, - onDismiss: dismiss, - ), - ), - ], - ), - ), - ), - ); - - Overlay.of(context).insert(_menuEntry!); - - keepEditorFocusNotifier.increase(); - // editorState.service.keyboardService?.disable(showCursor: true); - // editorState.service.scrollService?.disable(); - } -} - -class PasteAsMenu extends StatefulWidget { - const PasteAsMenu({ - super.key, - required this.onSelect, - required this.onDismiss, - required this.editorState, - }); - final ValueChanged onSelect; - final VoidCallback onDismiss; - final EditorState editorState; - - @override - State createState() => _PasteAsMenuState(); -} - -class _PasteAsMenuState extends State { - final focusNode = FocusNode(debugLabel: 'paste_as_menu'); - final ValueNotifier selectedIndexNotifier = ValueNotifier(0); - - EditorState get editorState => widget.editorState; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => focusNode.requestFocus(), - ); - editorState.selectionNotifier.addListener(dismiss); - } - - @override - void dispose() { - focusNode.dispose(); - selectedIndexNotifier.dispose(); - editorState.selectionNotifier.removeListener(dismiss); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Focus( - focusNode: focusNode, - onKeyEvent: onKeyEvent, - child: Container( - width: _menuWidth, - height: _menuHeighgt, - padding: EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: theme.surfaceColorScheme.primary, - boxShadow: theme.shadow.medium, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 32, - padding: EdgeInsets.all(8), - child: FlowyText.semibold( - color: theme.textColorScheme.primary, - LocaleKeys.document_plugins_linkPreview_typeSelection_pasteAs - .tr(), - ), - ), - ...List.generate( - PasteMenuType.values.length, - (i) => buildItem(PasteMenuType.values[i], i), - ), - ], - ), - ), - ); - } - - Widget buildItem(PasteMenuType type, int i) { - return ValueListenableBuilder( - valueListenable: selectedIndexNotifier, - builder: (context, value, child) { - final isSelected = i == value; - return SizedBox( - height: 36, - child: FlowyButton( - isSelected: isSelected, - text: FlowyText( - type.title, - ), - onTap: () => onSelect(type), - ), - ); - }, - ); - } - - void changeIndex(int index) => selectedIndexNotifier.value = index; - - KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - int index = selectedIndexNotifier.value, - length = PasteMenuType.values.length; - if (event.logicalKey == LogicalKeyboardKey.enter) { - onSelect(PasteMenuType.values[index]); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - dismiss(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - dismiss(); - } else if ([LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowLeft] - .contains(event.logicalKey)) { - if (index == 0) { - index = length - 1; - } else { - index--; - } - changeIndex(index); - return KeyEventResult.handled; - } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowRight] - .contains(event.logicalKey)) { - if (index == length - 1) { - index = 0; - } else { - index++; - } - changeIndex(index); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - void onSelect(PasteMenuType type) => widget.onSelect.call(type); - - void dismiss() => widget.onDismiss.call(); -} - -enum PasteMenuType { - mention, - url, - bookmark, - embed, -} - -extension PasteMenuTypeExtension on PasteMenuType { - String get title { - switch (this) { - case PasteMenuType.mention: - return LocaleKeys.document_plugins_linkPreview_typeSelection_mention - .tr(); - case PasteMenuType.url: - return LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr(); - case PasteMenuType.bookmark: - return LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark - .tr(); - case PasteMenuType.embed: - return LocaleKeys.document_plugins_linkPreview_typeSelection_embed.tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart index 8b193c70fb..57564c4722 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart @@ -1,5 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; @@ -11,7 +9,7 @@ Future convertUrlPreviewNodeToLink( return; } - final url = node.attributes[LinkPreviewBlockKeys.url]; + final url = node.attributes[ImageBlockKeys.url]; final delta = Delta() ..insert( url, @@ -31,172 +29,3 @@ Future convertUrlPreviewNodeToLink( ); return editorState.apply(transaction); } - -Future convertUrlPreviewNodeToMention( - EditorState editorState, - Node node, -) async { - if (node.type != LinkPreviewBlockKeys.type) { - return; - } - - final url = node.attributes[LinkPreviewBlockKeys.url]; - final delta = Delta() - ..insert( - MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.externalLink.name, - MentionBlockKeys.url: url, - }, - }, - ); - final transaction = editorState.transaction; - transaction - ..insertNode(node.path, paragraphNode(delta: delta)) - ..deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path, - offset: url.length, - ), - ); - return editorState.apply(transaction); -} - -Future removeUrlPreviewLink( - EditorState editorState, - Node node, -) async { - if (node.type != LinkPreviewBlockKeys.type) { - return; - } - - final url = node.attributes[LinkPreviewBlockKeys.url]; - final delta = Delta()..insert(url); - final transaction = editorState.transaction; - transaction - ..insertNode(node.path, paragraphNode(delta: delta)) - ..deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path, - offset: url.length, - ), - ); - return editorState.apply(transaction); -} - -Future convertUrlToLinkPreview( - EditorState editorState, - Selection selection, - String url, { - String? previewType, -}) async { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final delta = node.delta; - if (delta == null) return; - final List beforeOperations = [], afterOperations = []; - int index = 0; - for (final insert in delta.whereType()) { - if (index < selection.startIndex) { - beforeOperations.add(insert); - } else if (index >= selection.endIndex) { - afterOperations.add(insert); - } - index += insert.length; - } - final transaction = editorState.transaction; - transaction - ..deleteNode(node) - ..insertNodes(node.path.next, [ - if (beforeOperations.isNotEmpty) - paragraphNode(delta: Delta(operations: beforeOperations)), - if (previewType == LinkEmbedKeys.embed) - linkEmbedNode(url: url) - else - linkPreviewNode(url: url), - if (afterOperations.isNotEmpty) - paragraphNode(delta: Delta(operations: afterOperations)), - ]); - await editorState.apply(transaction); -} - -Future convertUrlToMention( - EditorState editorState, - Selection selection, -) async { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final delta = node.delta; - if (delta == null) return; - String url = ''; - int index = 0; - for (final insert in delta.whereType()) { - if (index >= selection.startIndex && index < selection.endIndex) { - final href = insert.attributes?.href ?? ''; - if (href.isNotEmpty) { - url = href; - break; - } - } - index += insert.length; - } - final transaction = editorState.transaction; - transaction.replaceText( - node, - selection.startIndex, - selection.length, - MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.externalLink.name, - MentionBlockKeys.url: url, - }, - }, - ); - await editorState.apply(transaction); -} - -Future convertLinkBlockToOtherLinkBlock( - EditorState editorState, - Node node, - String toType, { - String? url, -}) async { - final nodeType = node.type; - if (nodeType != LinkPreviewBlockKeys.type || - (nodeType == toType && url == null)) { - return; - } - final insertedNode = []; - - final afterUrl = url ?? node.attributes[LinkPreviewBlockKeys.url] ?? ''; - final previewType = node.attributes[LinkEmbedKeys.previewType]; - Node afterNode = node.copyWith( - type: toType, - attributes: { - LinkPreviewBlockKeys.url: afterUrl, - LinkEmbedKeys.previewType: previewType, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: node.attributes[blockComponentTextDirection], - blockComponentDelta: (node.delta ?? Delta()).toJson(), - }, - ); - afterNode = afterNode.copyWith(children: []); - insertedNode.add(afterNode); - insertedNode.addAll(node.children.map((e) => e.deepCopy())); - final transaction = editorState.transaction; - transaction.insertNodes( - node.path, - insertedNode, - ); - transaction.deleteNodes([node]); - await editorState.apply(transaction); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 2f724061ee..e9d6b3297e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -79,10 +79,6 @@ class MathEquationBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -98,7 +94,6 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -162,7 +157,6 @@ class MathEquationBlockComponentWidgetState child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart index 77f8c8d0a1..ef32ad1098 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart @@ -100,6 +100,7 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { Log.error(error); if (context.mounted) { showToastNotification( + context, message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage .tr(), ); @@ -178,6 +179,13 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { await duplicatedViewOrFailure.fold( (newView) async { + final newMentionAttributes = { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.childPage.name, + MentionBlockKeys.pageId: newView.id, + }, + }; + // The index is the index of the delta, to get the index of the mention character // in all the text, we need to calculate it based on the deltas before the current delta. int mentionIndex = 0; @@ -194,11 +202,7 @@ class ChildPageTransactionHandler extends MentionTransactionHandler { node, mentionIndex, MentionBlockKeys.mentionChar.length, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.childPage, - pageId: newView.id, - blockId: null, - ), + newMentionAttributes, ); await editorState.apply( transaction, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart index cb3196e9b7..972ed229dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart @@ -192,12 +192,15 @@ class DateTransactionHandler extends MentionTransactionHandler { ), ); - final newMentionAttributes = MentionBlockKeys.buildMentionDateAttributes( - date: dateTime.toIso8601String(), - reminderId: reminderId, - reminderOption: data.reminderOption.name, - includeTime: data.includeTime, - ); + final newMentionAttributes = { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: dateTime.toIso8601String(), + MentionBlockKeys.reminderId: reminderId, + MentionBlockKeys.includeTime: data.includeTime, + MentionBlockKeys.reminderOption: data.reminderOption.name, + }, + }; // The index is the index of the delta, to get the index of the mention character // in all the text, we need to calculate it based on the deltas before the current delta. diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 0060d65bb7..d65609f1a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -6,18 +6,14 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'mention_link_block.dart'; - enum MentionType { page, date, - externalLink, childPage; static MentionType fromString(String value) => switch (value) { 'page' => page, 'date' => date, - 'externalLink' => externalLink, 'childPage' => childPage, // Backwards compatibility 'reminder' => date, @@ -31,12 +27,12 @@ Node dateMentionNode() { operations: [ TextInsert( MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: DateTime.now().toIso8601String(), - reminderId: null, - reminderOption: null, - includeTime: false, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: DateTime.now().toIso8601String(), + }, + }, ), ], ), @@ -46,52 +42,18 @@ Node dateMentionNode() { class MentionBlockKeys { const MentionBlockKeys._(); + static const reminderId = 'reminder_id'; // ReminderID static const mention = 'mention'; static const type = 'type'; // MentionType, String - static const pageId = 'page_id'; static const blockId = 'block_id'; - static const url = 'url'; // Related to Reminder and Date blocks static const date = 'date'; // Start Date static const includeTime = 'include_time'; - static const reminderId = 'reminder_id'; // ReminderID static const reminderOption = 'reminder_option'; static const mentionChar = '\$'; - - static Map buildMentionPageAttributes({ - required MentionType mentionType, - required String pageId, - required String? blockId, - }) { - return { - MentionBlockKeys.mention: { - MentionBlockKeys.type: mentionType.name, - MentionBlockKeys.pageId: pageId, - if (blockId != null) MentionBlockKeys.blockId: blockId, - }, - }; - } - - static Map buildMentionDateAttributes({ - required String date, - required String? reminderId, - required String? reminderOption, - required bool includeTime, - }) { - return { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: date, - MentionBlockKeys.includeTime: includeTime, - if (reminderId != null) MentionBlockKeys.reminderId: reminderId, - if (reminderOption != null) - MentionBlockKeys.reminderOption: reminderOption, - }, - }; - } } class MentionBlock extends StatelessWidget { @@ -162,17 +124,6 @@ class MentionBlock extends StatelessWidget { reminderOption: reminderOption ?? ReminderOption.none, includeTime: mention[MentionBlockKeys.includeTime] ?? false, ); - case MentionType.externalLink: - final String? url = mention[MentionBlockKeys.url] as String?; - if (url == null) { - return const SizedBox.shrink(); - } - return MentionLinkBlock( - url: url, - editorState: editorState, - node: node, - index: index, - ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 20f60be23d..e1df115e15 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -60,6 +60,8 @@ class MentionDateBlock extends StatefulWidget { } class _MentionDateBlockState extends State { + final PopoverMutex mutex = PopoverMutex(); + late bool _includeTime = widget.includeTime; late DateTime? parsedDate = DateTime.tryParse(widget.date); @@ -69,6 +71,12 @@ class _MentionDateBlockState extends State { super.didUpdateWidget(oldWidget); } + @override + void dispose() { + mutex.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (parsedDate == null) { @@ -97,6 +105,7 @@ class _MentionDateBlockState extends State { final options = DatePickerOptions( focusedDay: parsedDate, + popoverMutex: mutex, selectedDay: parsedDate, includeTime: _includeTime, dateFormat: appearance.dateFormat, @@ -201,17 +210,16 @@ class _MentionDateBlockState extends State { (reminderOption == ReminderOption.none ? null : widget.reminderId); final transaction = widget.editorState.transaction - ..formatText( - widget.node, - widget.index, - 1, - MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - reminderId: rId, - includeTime: includeTime, - reminderOption: reminderOption?.name ?? widget.reminderOption.name, - ), - ); + ..formatText(widget.node, widget.index, 1, { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + MentionBlockKeys.reminderId: rId, + MentionBlockKeys.includeTime: includeTime, + MentionBlockKeys.reminderOption: + reminderOption?.name ?? widget.reminderOption.name, + }, + }); widget.editorState.apply(transaction, withUpdateSelection: false); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart deleted file mode 100644 index 06ebcb5002..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'mention_link_error_preview.dart'; -import 'mention_link_preview.dart'; - -class MentionLinkBlock extends StatefulWidget { - const MentionLinkBlock({ - super.key, - required this.url, - required this.editorState, - required this.node, - required this.index, - this.delayToShow = const Duration(milliseconds: 50), - this.delayToHide = const Duration(milliseconds: 300), - }); - - final String url; - final Duration delayToShow; - final Duration delayToHide; - final EditorState editorState; - final Node node; - final int index; - - @override - State createState() => _MentionLinkBlockState(); -} - -class _MentionLinkBlockState extends State { - final parser = LinkParser(); - _LoadingStatus status = _LoadingStatus.loading; - late LinkInfo linkInfo = LinkInfo(url: url); - final previewController = PopoverController(); - bool isHovering = false; - int previewFocusNum = 0; - bool isPreviewHovering = false; - bool showAtBottom = false; - final key = GlobalKey(); - - bool get isPreviewShowing => previewFocusNum > 0; - String get url => widget.url; - - EditorState get editorState => widget.editorState; - - Node get node => widget.node; - - int get index => widget.index; - - bool get readyForPreview => - status == _LoadingStatus.idle && !linkInfo.isEmpty(); - - @override - void initState() { - super.initState(); - - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = _LoadingStatus.idle; - } else if (!hasOldInfo) { - status = _LoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - super.dispose(); - parser.dispose(); - previewController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - key: ValueKey(showAtBottom), - controller: previewController, - direction: showAtBottom - ? PopoverDirection.bottomWithLeftAligned - : PopoverDirection.topWithLeftAligned, - offset: Offset(0, showAtBottom ? -20 : 20), - onOpen: () { - keepEditorFocusNotifier.increase(); - previewFocusNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - previewFocusNum--; - }, - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - margin: EdgeInsets.zero, - constraints: getConstraints(), - borderRadius: BorderRadius.circular(16), - popupBuilder: (context) => readyForPreview - ? MentionLinkPreview( - linkInfo: linkInfo, - showAtBottom: showAtBottom, - triggerSize: getSizeFromKey(), - onEnter: (e) { - isPreviewHovering = true; - }, - onExit: (e) { - isPreviewHovering = false; - tryToDismissPreview(); - }, - onCopyLink: () => copyLink(context), - onConvertTo: (s) => convertTo(s), - onRemoveLink: removeLink, - onOpenLink: openLink, - ) - : MentionLinkErrorPreview( - url: url, - triggerSize: getSizeFromKey(), - onEnter: (e) { - isPreviewHovering = true; - }, - onExit: (e) { - isPreviewHovering = false; - tryToDismissPreview(); - }, - onCopyLink: () => copyLink(context), - onConvertTo: (s) => convertTo(s), - onRemoveLink: removeLink, - onOpenLink: openLink, - ), - child: buildIconWithTitle(context), - ); - } - - Widget buildIconWithTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final siteName = linkInfo.siteName, linkTitle = linkInfo.title ?? url; - - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: onEnter, - onExit: onExit, - child: GestureDetector( - onTap: () async { - await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - }, - child: FlowyHoverContainer( - style: - HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary), - applyStyle: isHovering, - child: Row( - mainAxisSize: MainAxisSize.min, - key: key, - children: [ - HSpace(2), - buildIcon(), - HSpace(4), - Flexible( - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - children: [ - if (siteName != null) ...[ - TextSpan( - text: siteName, - style: theme.textStyle.body - .standard(color: theme.textColorScheme.secondary), - ), - WidgetSpan(child: HSpace(2)), - ], - TextSpan( - text: linkTitle, - style: theme.textStyle.body - .standard(color: theme.textColorScheme.primary), - ), - ], - ), - ), - ), - HSpace(2), - ], - ), - ), - ), - ); - } - - Widget buildIcon() { - const defaultWidget = FlowySvg(FlowySvgs.toolbar_link_earth_m); - Widget icon = defaultWidget; - if (status == _LoadingStatus.loading) { - icon = Padding( - padding: const EdgeInsets.all(2.0), - child: const CircularProgressIndicator(strokeWidth: 1), - ); - } else { - icon = linkInfo.buildIconWidget(); - } - return SizedBox( - height: 20, - width: 20, - child: icon, - ); - } - - RenderBox? get box => key.currentContext?.findRenderObject() as RenderBox?; - - Size getSizeFromKey() => box?.size ?? Size.zero; - - Future copyLink(BuildContext context) async { - await context.copyLink(url); - previewController.close(); - } - - Future openLink() async { - await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - } - - Future removeLink() async { - final transaction = editorState.transaction - ..replaceText(widget.node, widget.index, 1, url, attributes: {}); - await editorState.apply(transaction); - } - - Future convertTo(PasteMenuType type) async { - if (type == PasteMenuType.url) { - await toUrl(); - } else if (type == PasteMenuType.bookmark) { - await toLinkPreview(); - } else if (type == PasteMenuType.embed) { - await toLinkPreview(previewType: LinkEmbedKeys.embed); - } - } - - Future toUrl() async { - final transaction = editorState.transaction - ..replaceText( - widget.node, - widget.index, - 1, - url, - attributes: { - AppFlowyRichTextKeys.href: url, - }, - ); - await editorState.apply(transaction); - } - - Future toLinkPreview({String? previewType}) async { - final selection = Selection( - start: Position(path: node.path, offset: index), - end: Position(path: node.path, offset: index + 1), - ); - await convertUrlToLinkPreview( - editorState, - selection, - url, - previewType: previewType, - ); - } - - void changeHovering(bool hovering) { - if (isHovering == hovering) return; - if (mounted) { - setState(() { - isHovering = hovering; - }); - } - } - - void changeShowAtBottom(bool bottom) { - if (showAtBottom == bottom) return; - if (mounted) { - setState(() { - showAtBottom = bottom; - }); - } - } - - void tryToDismissPreview() { - Future.delayed(widget.delayToHide, () { - if (isHovering || isPreviewHovering) { - return; - } - previewController.close(); - }); - } - - void onEnter(PointerEnterEvent e) { - changeHovering(true); - final location = box?.localToGlobal(Offset.zero) ?? Offset.zero; - if (readyForPreview) { - if (location.dy < 300) { - changeShowAtBottom(true); - } else { - changeShowAtBottom(false); - } - } - Future.delayed(widget.delayToShow, () { - if (isHovering && !isPreviewShowing && status != _LoadingStatus.loading) { - showPreview(); - } - }); - } - - void onExit(PointerExitEvent e) { - changeHovering(false); - tryToDismissPreview(); - } - - void showPreview() { - if (!mounted) return; - keepEditorFocusNotifier.increase(); - previewController.show(); - previewFocusNum++; - } - - BoxConstraints getConstraints() { - final size = getSizeFromKey(); - if (!readyForPreview) { - return BoxConstraints( - maxWidth: max(320, size.width), - maxHeight: 48 + size.height, - ); - } - final hasImage = linkInfo.imageUrl?.isNotEmpty ?? false; - return BoxConstraints( - maxWidth: max(300, size.width), - maxHeight: hasImage ? 300 : 180, - ); - } -} - -enum _LoadingStatus { - loading, - idle, - error, -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart deleted file mode 100644 index df396108e4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class MentionLinkErrorPreview extends StatefulWidget { - const MentionLinkErrorPreview({ - super.key, - required this.url, - required this.onEnter, - required this.onExit, - required this.onCopyLink, - required this.onRemoveLink, - required this.onConvertTo, - required this.onOpenLink, - required this.triggerSize, - }); - - final String url; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final VoidCallback onCopyLink; - final VoidCallback onRemoveLink; - final VoidCallback onOpenLink; - final ValueChanged onConvertTo; - final Size triggerSize; - - @override - State createState() => - _MentionLinkErrorPreviewState(); -} - -class _MentionLinkErrorPreviewState extends State { - final menuController = PopoverController(); - bool isConvertButtonSelected = false; - - @override - void dispose() { - super.dispose(); - menuController.close(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: SizedBox( - width: max(320, widget.triggerSize.width), - height: 48, - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: 320, - height: 48, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.fromLTRB(12, 8, 8, 8), - child: Row( - children: [ - Expanded(child: buildLinkWidget()), - Container( - height: 20, - width: 1, - color: Color(0xffE8ECF3) - .withAlpha(Theme.of(context).isLightMode ? 255 : 40), - margin: EdgeInsets.symmetric(horizontal: 6), - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_m), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onCopyLink, - ), - buildConvertButton(), - ], - ), - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - onTap: widget.onOpenLink, - child: Container( - width: widget.triggerSize.width, - height: widget.triggerSize.height, - color: Colors.black.withAlpha(1), - ), - ), - ), - ], - ); - } - - Widget buildLinkWidget() { - final url = widget.url; - return FlowyTooltip( - message: url, - preferBelow: false, - child: FlowyText.regular( - url, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ); - } - - Widget buildConvertButton() { - return AppFlowyPopover( - offset: Offset(8, 10), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: menuController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg(FlowySvgs.turninto_m), - isSelected: isConvertButtonSelected, - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: () { - setState(() { - isConvertButtonSelected = true; - }); - showPopover(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(MentionLinktErrorMenuCommand.values.length, - (index) { - final command = MentionLinktErrorMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - menuController.show(); - } - - void closePopover() { - menuController.close(); - } - - void onTap(MentionLinktErrorMenuCommand command) { - switch (command) { - case MentionLinktErrorMenuCommand.toURL: - widget.onConvertTo(PasteMenuType.url); - break; - case MentionLinktErrorMenuCommand.toBookmark: - widget.onConvertTo(PasteMenuType.bookmark); - break; - case MentionLinktErrorMenuCommand.toEmbed: - widget.onConvertTo(PasteMenuType.embed); - break; - case MentionLinktErrorMenuCommand.removeLink: - widget.onRemoveLink(); - break; - } - closePopover(); - } -} - -enum MentionLinktErrorMenuCommand { - toURL, - toBookmark, - toEmbed, - removeLink; - - String get title { - switch (this) { - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart deleted file mode 100644 index 00b161379e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart +++ /dev/null @@ -1,276 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class MentionLinkPreview extends StatefulWidget { - const MentionLinkPreview({ - super.key, - required this.linkInfo, - required this.onEnter, - required this.onExit, - required this.onCopyLink, - required this.onRemoveLink, - required this.onConvertTo, - required this.onOpenLink, - required this.triggerSize, - required this.showAtBottom, - }); - - final LinkInfo linkInfo; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final VoidCallback onCopyLink; - final VoidCallback onRemoveLink; - final VoidCallback onOpenLink; - final ValueChanged onConvertTo; - final Size triggerSize; - final bool showAtBottom; - - @override - State createState() => _MentionLinkPreviewState(); -} - -class _MentionLinkPreviewState extends State { - final menuController = PopoverController(); - bool isSelected = false; - - LinkInfo get linkInfo => widget.linkInfo; - - @override - void dispose() { - super.dispose(); - menuController.close(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context), - textColorScheme = theme.textColorScheme; - final imageUrl = linkInfo.imageUrl ?? '', - description = linkInfo.description ?? ''; - final imageHeight = 120.0; - final card = MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Container( - decoration: buildToolbarLinkDecoration(context, radius: 16), - width: 280, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (imageUrl.isNotEmpty) - ClipRRect( - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16)), - child: FlowyNetworkImage( - url: linkInfo.imageUrl ?? '', - width: 280, - height: imageHeight, - ), - ), - VSpace(12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyText.semibold( - linkInfo.title ?? linkInfo.siteName ?? '', - fontSize: 14, - figmaLineHeight: 20, - color: textColorScheme.primary, - overflow: TextOverflow.ellipsis, - ), - ), - VSpace(4), - if (description.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyText( - description, - fontSize: 12, - figmaLineHeight: 16, - color: textColorScheme.secondary, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - VSpace(36), - ], - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - height: 28, - child: Row( - children: [ - linkInfo.buildIconWidget(size: Size.square(16)), - HSpace(6), - Expanded( - child: FlowyText( - linkInfo.siteName ?? linkInfo.url, - fontSize: 12, - figmaLineHeight: 16, - color: textColorScheme.primary, - overflow: TextOverflow.ellipsis, - fontWeight: FontWeight.w700, - ), - ), - buildMoreOptionButton(), - ], - ), - ), - VSpace(12), - ], - ), - ), - ); - - final clickPlaceHolder = MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - child: Container( - height: 20, - width: widget.triggerSize.width, - color: Colors.white.withAlpha(1), - ), - onTap: () { - widget.onOpenLink.call(); - closePopover(); - }, - ), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: widget.showAtBottom - ? [clickPlaceHolder, card] - : [card, clickPlaceHolder], - ); - } - - Widget buildMoreOptionButton() { - return AppFlowyPopover( - controller: menuController, - direction: PopoverDirection.topWithLeftAligned, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - margin: EdgeInsets.zero, - borderRadius: BorderRadius.circular(12), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - width: 28, - height: 28, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - size: Size.square(20), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(MentionLinktMenuCommand.values.length, (index) { - final command = MentionLinktMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - menuController.show(); - } - - void closePopover() { - menuController.close(); - } - - void onTap(MentionLinktMenuCommand command) { - switch (command) { - case MentionLinktMenuCommand.toURL: - widget.onConvertTo(PasteMenuType.url); - break; - case MentionLinktMenuCommand.toBookmark: - widget.onConvertTo(PasteMenuType.bookmark); - break; - case MentionLinktMenuCommand.toEmbed: - widget.onConvertTo(PasteMenuType.embed); - break; - case MentionLinktMenuCommand.copyLink: - widget.onCopyLink(); - break; - case MentionLinktMenuCommand.removeLink: - widget.onRemoveLink(); - break; - } - closePopover(); - } -} - -enum MentionLinktMenuCommand { - toURL, - toBookmark, - toEmbed, - copyLink, - removeLink; - - String get title { - switch (this) { - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case copyLink: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index ede690eb30..398d158e9a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -50,11 +50,12 @@ Node pageMentionNode(String viewId) { operations: [ TextInsert( MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: viewId, - blockId: null, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: viewId, + }, + }, ), ], ), @@ -117,7 +118,7 @@ class _MentionPageBlockState extends State { view: view, content: state.blockContent, textStyle: widget.textStyle, - handleTap: () => handleMentionBlockTap( + handleTap: () => _handleTap( context, widget.editorState, view, @@ -137,7 +138,7 @@ class _MentionPageBlockState extends State { content: state.blockContent, textStyle: widget.textStyle, showTrashHint: state.isInTrash, - handleTap: () => handleMentionBlockTap( + handleTap: () => _handleTap( context, widget.editorState, view, @@ -220,8 +221,7 @@ class _MentionSubPageBlockState extends State { view: view, showTrashHint: state.isInTrash, textStyle: widget.textStyle, - handleTap: () => - handleMentionBlockTap(context, widget.editorState, view), + handleTap: () => _handleTap(context, widget.editorState, view), isChildPage: true, content: '', handleDoubleTap: () => _handleDoubleTap( @@ -239,8 +239,7 @@ class _MentionSubPageBlockState extends State { content: null, textStyle: widget.textStyle, isChildPage: true, - handleTap: () => - handleMentionBlockTap(context, widget.editorState, view), + handleTap: () => _handleTap(context, widget.editorState, view), ); } }, @@ -283,11 +282,12 @@ class _MentionSubPageBlockState extends State { widget.node, widget.index, MentionBlockKeys.mentionChar.length, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: widget.pageId, - blockId: null, - ), + { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: widget.pageId, + }, + }, ); widget.editorState.apply( @@ -321,7 +321,7 @@ Path? _findNodePathByBlockId(EditorState editorState, String blockId) { return null; } -Future handleMentionBlockTap( +Future _handleTap( BuildContext context, EditorState editorState, ViewPB view, { @@ -381,24 +381,25 @@ Future _handleDoubleTap( } final currentViewId = context.read().documentId; - final newView = await showPageSelectorSheet( + final newViewId = await showPageSelectorSheet( context, currentViewId: currentViewId, selectedViewId: viewId, ); - if (newView != null) { + if (newViewId != null) { // Update this nodes pageId final transaction = editorState.transaction ..formatText( node, index, 1, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: newView.id, - blockId: null, - ), + { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: newViewId, + }, + }, ); await editorState.apply(transaction, withUpdateSelection: false); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart deleted file mode 100644 index 7dcd21f423..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -extension MenuExtension on EditorState { - MenuPosition? calculateMenuOffset({ - Rect? rect, - required double menuWidth, - required double menuHeight, - Offset menuOffset = const Offset(0, 10), - }) { - final selectionService = service.selectionService; - final selectionRects = selectionService.selectionRects; - late Rect startRect; - if (rect != null) { - startRect = rect; - } else { - if (selectionRects.isEmpty) return null; - startRect = selectionRects.first; - } - - final editorOffset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = renderBox!.size.height; - final editorWidth = renderBox!.size.width; - - // show below default - Alignment alignment = Alignment.topLeft; - final bottomRight = startRect.bottomRight; - final topRight = startRect.topRight; - var startOffset = bottomRight + menuOffset; - Offset offset = Offset( - startOffset.dx, - startOffset.dy, - ); - - // show above - if (startOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { - startOffset = topRight - menuOffset; - alignment = Alignment.bottomLeft; - - offset = Offset( - startOffset.dx, - editorHeight + editorOffset.dy - startOffset.dy, - ); - } - - // show on right - if (offset.dx + menuWidth < editorOffset.dx + editorWidth) { - offset = Offset( - offset.dx, - offset.dy, - ); - } else if (startOffset.dx - editorOffset.dx > menuWidth) { - // show on left - alignment = alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - offset = Offset( - editorWidth - offset.dx + editorOffset.dx, - offset.dy, - ); - } - return MenuPosition(align: alignment, offset: offset); - } -} - -class MenuPosition { - MenuPosition({ - required this.align, - required this.offset, - }); - - final Alignment align; - final Offset offset; - - LTRB get ltrb { - double? left, top, right, bottom; - switch (align) { - case Alignment.topLeft: - left = offset.dx; - top = offset.dy; - break; - case Alignment.bottomLeft: - left = offset.dx; - bottom = offset.dy; - break; - case Alignment.topRight: - right = offset.dx; - top = offset.dy; - break; - case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; - break; - } - - return LTRB(left: left, top: top, right: right, bottom: bottom); - } -} - -class LTRB { - LTRB({this.left, this.top, this.right, this.bottom}); - - final double? left; - final double? top; - final double? right; - final double? bottom; - - Positioned buildPositioned({required Widget child}) => Positioned( - left: left, - top: top, - right: right, - bottom: bottom, - child: child, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart index 41bb8ce873..be9063a6c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart @@ -7,8 +7,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; import 'package:string_validator/string_validator.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart index 8e1a8533e0..57670afadd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart @@ -6,8 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart index b3df4dfd39..4ae243575c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart @@ -12,8 +12,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart index f77083d21d..2f31a68a73 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -30,9 +29,9 @@ class NumberedListIcon extends StatelessWidget { ); return Padding( - padding: const EdgeInsets.only(left: 6.0, right: 10.0), + padding: const EdgeInsets.only(right: 8.0), child: Text( - node.buildLevelString(context), + node.levelString, style: adjustedTextStyle, strutStyle: StrutStyle.fromTextStyle(combinedTextStyle), textHeightBehavior: TextHeightBehavior( @@ -48,12 +47,9 @@ class NumberedListIcon extends StatelessWidget { } } -extension NumberedListNodeIndex on Node { - String buildLevelString(BuildContext context) { - final builder = NumberedListIndexBuilder( - editorState: context.read(), - node: this, - ); +extension on Node { + String get levelString { + final builder = _NumberedListIconBuilder(node: this); final indexInRootLevel = builder.indexInRootLevel; final indexInSameLevel = builder.indexInSameLevel; final level = indexInRootLevel % 3; @@ -66,13 +62,11 @@ extension NumberedListNodeIndex on Node { } } -class NumberedListIndexBuilder { - NumberedListIndexBuilder({ - required this.editorState, +class _NumberedListIconBuilder { + _NumberedListIconBuilder({ required this.node, }); - final EditorState editorState; final Node node; // the level of the current node @@ -94,13 +88,7 @@ class NumberedListIndexBuilder { Node? previous = node.previous; // if the previous one is not a numbered list, then it is the first one - final aiNodeExternalValues = - node.externalValues?.unwrapOrNull(); - - if (previous == null || - previous.type != NumberedListBlockKeys.type || - (aiNodeExternalValues != null && - aiNodeExternalValues.isFirstNumberedListNode)) { + if (previous == null || previous.type != NumberedListBlockKeys.type) { return node.attributes[NumberedListBlockKeys.number] ?? level; } @@ -109,17 +97,10 @@ class NumberedListIndexBuilder { startNumber = previous.attributes[NumberedListBlockKeys.number] as int?; level++; previous = previous.previous; - - // break the loop if the start number is found when the current node is an AI node - if (aiNodeExternalValues != null && startNumber != null) { - return startNumber + level - 1; - } } - if (startNumber != null) { - level = startNumber + level - 1; + return startNumber + level - 1; } - return level; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index e3120356d9..eaacb43ea6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -52,10 +52,6 @@ class OutlineBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -69,7 +65,6 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -81,9 +76,7 @@ class _OutlineBlockWidgetState extends State with BlockComponentConfigurable, BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin, - DefaultSelectableMixin, - SelectableMixin { + BlockComponentBackgroundColorMixin { // Change the value if the heading block type supports heading levels greater than '3' static const maxVisibleDepth = 6; @@ -97,17 +90,6 @@ class _OutlineBlockWidgetState extends State late EditorState editorState = context.read(); late Stream stream = editorState.transactionStream; - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: OutlineBlockKeys.type, - ); - - @override - GlobalKey> get containerKey => widget.node.key; - - @override - GlobalKey> get forwardKey => widget.node.key; - @override Widget build(BuildContext context) { return StreamBuilder( @@ -115,25 +97,11 @@ class _OutlineBlockWidgetState extends State builder: (context, snapshot) { Widget child = _buildOutlineBlock(); - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - remoteSelection: editorState.remoteSelections, - blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, - supportTypes: const [ - BlockSelectionType.block, - ], - child: child, - ); - if (UniversalPlatform.isDesktopOrWeb) { if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -202,7 +170,6 @@ class _OutlineBlockWidgetState extends State } return Container( - key: blockComponentKey, constraints: const BoxConstraints( minHeight: 40.0, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart index 731ba4c7cd..5d7028f6e1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart @@ -25,7 +25,6 @@ class CustomPageBlockComponent extends BlockComponentStatelessWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.header, this.footer, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 45a23bc6ac..27498cc65e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -226,7 +226,7 @@ class PageStyleCoverImage extends StatelessWidget { (f) => null, ); final isAppFlowyCloud = - userProfile?.workspaceAuthType == AuthTypePB.Server; + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index d9cf060e3b..867fcf236f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; class CalloutNodeParser extends NodeParser { @@ -10,6 +9,8 @@ class CalloutNodeParser extends NodeParser { @override String transform(Node node, DocumentMarkdownEncoder? encoder) { + assert(node.children.isEmpty); + final icon = node.attributes[CalloutBlockKeys.icon]; final delta = node.delta ?? Delta() ..insert(''); final String markdown = DeltaMarkdownEncoder() @@ -17,15 +18,9 @@ class CalloutNodeParser extends NodeParser { .split('\n') .map((e) => '> $e') .join('\n'); - final type = node.attributes[CalloutBlockKeys.iconType]; - final icon = type == FlowyIconType.emoji.name || type == null || type == "" - ? node.attributes[CalloutBlockKeys.icon] - : null; - - final content = icon == null ? markdown : "> $icon\n$markdown"; - return ''' -$content +> $icon +$markdown '''; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 4161036a08..8a475fd5a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -40,6 +40,7 @@ export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'keyboard_interceptor/keyboard_interceptor.dart'; export 'link_preview/custom_link_preview.dart'; +export 'link_preview/link_preview_cache.dart'; export 'link_preview/link_preview_menu.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; @@ -62,7 +63,6 @@ export 'outline/outline_block_component.dart'; export 'parsers/document_markdown_parsers.dart'; export 'parsers/markdown_parsers.dart'; export 'parsers/markdown_simple_table_parser.dart'; -export 'quote/quote_block_component.dart'; export 'quote/quote_block_shortcuts.dart'; export 'shortcuts/character_shortcuts.dart'; export 'shortcuts/command_shortcuts.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart deleted file mode 100644 index 39ab2c5327..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// In memory cache of the quote block height to avoid flashing when the quote block is updated. -Map _quoteBlockHeightCache = {}; - -typedef QuoteBlockIconBuilder = Widget Function( - BuildContext context, - Node node, -); - -class QuoteBlockKeys { - const QuoteBlockKeys._(); - - static const String type = 'quote'; - - static const String delta = blockComponentDelta; - - static const String backgroundColor = blockComponentBackgroundColor; - - static const String textDirection = blockComponentTextDirection; -} - -Node quoteNode({ - Delta? delta, - String? textDirection, - Attributes? attributes, - Iterable? children, -}) { - attributes ??= {'delta': (delta ?? Delta()).toJson()}; - return Node( - type: QuoteBlockKeys.type, - attributes: { - ...attributes, - if (textDirection != null) QuoteBlockKeys.textDirection: textDirection, - }, - children: children ?? [], - ); -} - -class QuoteBlockComponentBuilder extends BlockComponentBuilder { - QuoteBlockComponentBuilder({ - super.configuration, - this.iconBuilder, - }); - - final QuoteBlockIconBuilder? iconBuilder; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return QuoteBlockComponentWidget( - key: node.key, - node: node, - configuration: configuration, - iconBuilder: iconBuilder, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.delta != null; -} - -class QuoteBlockComponentWidget extends BlockComponentStatefulWidget { - const QuoteBlockComponentWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - this.iconBuilder, - }); - - final QuoteBlockIconBuilder? iconBuilder; - - @override - State createState() => - _QuoteBlockComponentWidgetState(); -} - -class _QuoteBlockComponentWidgetState extends State - with - SelectableMixin, - DefaultSelectableMixin, - BlockComponentConfigurable, - BlockComponentBackgroundColorMixin, - BlockComponentTextDirectionMixin, - BlockComponentAlignMixin, - NestedBlockComponentStatefulWidgetMixin { - @override - final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); - - @override - GlobalKey> get containerKey => widget.node.key; - - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: QuoteBlockKeys.type, - ); - - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - late ValueNotifier quoteBlockHeightNotifier = ValueNotifier( - _quoteBlockHeightCache[node.id] ?? 0, - ); - - StreamSubscription? _transactionSubscription; - - final GlobalKey layoutBuilderKey = GlobalKey(); - - @override - void initState() { - super.initState(); - - _updateQuoteBlockHeight(); - } - - @override - void dispose() { - _transactionSubscription?.cancel(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return NotificationListener( - key: layoutBuilderKey, - onNotification: (notification) { - _updateQuoteBlockHeight(); - return true; - }, - child: SizeChangedLayoutNotifier( - child: node.children.isEmpty - ? buildComponent(context) - : buildComponentWithChildren(context), - ), - ); - } - - @override - Widget buildComponentWithChildren(BuildContext context) { - final Widget child = Stack( - children: [ - Positioned.fill( - left: UniversalPlatform.isMobile ? padding.left : cachedLeft, - right: UniversalPlatform.isMobile ? padding.right : 0, - child: Container( - color: backgroundColor, - ), - ), - NestedListWidget( - indentPadding: indentPadding, - child: buildComponent(context, withBackgroundColor: false), - children: editorState.renderer.buildList( - context, - widget.node.children, - ), - ), - ], - ); - - return child; - } - - @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = true, - }) { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - - Widget child = AppFlowyRichText( - key: forwardKey, - delegate: this, - node: widget.node, - editorState: editorState, - textAlign: alignment?.toTextAlign ?? textAlign, - placeholderText: placeholderText, - textSpanDecorator: (textSpan) => textSpan.updateTextStyle( - textStyleWithTextSpan(textSpan: textSpan), - ), - placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( - placeholderTextStyleWithTextSpan(textSpan: textSpan), - ), - textDirection: textDirection, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - cursorWidth: editorState.editorStyle.cursorWidth, - ); - - child = Container( - width: double.infinity, - alignment: alignment, - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - textDirection: textDirection, - children: [ - widget.iconBuilder != null - ? widget.iconBuilder!(context, node) - : ValueListenableBuilder( - valueListenable: quoteBlockHeightNotifier, - builder: (context, height, child) { - return QuoteIcon(height: height); - }, - ), - Flexible( - child: child, - ), - ], - ), - ), - ); - - child = Container( - color: withBackgroundColor ? backgroundColor : null, - child: Padding( - key: blockComponentKey, - padding: padding, - child: child, - ), - ); - - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - remoteSelection: editorState.remoteSelections, - blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, - supportTypes: const [ - BlockSelectionType.block, - ], - child: child, - ); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - return child; - } - - void _updateQuoteBlockHeight() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final renderObject = layoutBuilderKey.currentContext?.findRenderObject(); - double height = _quoteBlockHeightCache[node.id] ?? 0; - if (renderObject != null && renderObject is RenderBox) { - if (UniversalPlatform.isMobile) { - height = renderObject.size.height - padding.top; - } else { - height = renderObject.size.height - padding.top * 2; - } - } else { - height = 0; - } - - quoteBlockHeightNotifier.value = height; - _quoteBlockHeightCache[node.id] = height; - }); - } -} - -class QuoteIcon extends StatelessWidget { - const QuoteIcon({ - super.key, - this.height = 0, - }); - - final double height; - - @override - Widget build(BuildContext context) { - final textScaleFactor = - context.read().editorStyle.textScaleFactor; - return Container( - alignment: Alignment.center, - constraints: - const BoxConstraints(minWidth: 22, minHeight: 22, maxHeight: 22) * - textScaleFactor, - padding: const EdgeInsets.only(right: 6.0), - child: SizedBox( - width: 3 * textScaleFactor, - // use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote - child: OverflowBox( - alignment: Alignment.topCenter, - maxHeight: height, - child: Container( - width: 3 * textScaleFactor, - height: height, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart index 47c6549923..ba3ad6e7df 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart @@ -30,25 +30,10 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { await editorState.deleteSelection(selection); if (HardwareKeyboard.instance.isShiftPressed) { - // ignore the shift+enter event, fallback to the default behavior - return false; - } else if (node.children.isEmpty && - selection.endIndex == node.delta?.length) { - // insert a new paragraph within the callout block - final path = node.path.child(0); - final transaction = editorState.transaction; - transaction.insertNode( - path, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - await editorState.apply(transaction); - return true; + await editorState.insertNewLine(); + } else { + await editorState.insertTextAtCurrentSelection('\n'); } - return false; + return true; }; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart index 13b2fea5ee..49178ca12b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart @@ -7,7 +7,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -83,9 +82,5 @@ List buildCharacterShortcutEvents( documentBloc.documentId, styleCustomizer.inlineActionsMenuStyleBuilder(), ), - - /// show emoji list - /// - Using `:` - emojiCommand(context), ]; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart index ee6020793c..5d4c3cab52 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart @@ -161,10 +161,6 @@ class SimpleTableBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -178,7 +174,6 @@ class SimpleTableBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); @@ -267,7 +262,6 @@ class _SimpleTableBlockWidgetState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart index 29b3c3455f..112343c6d8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart @@ -47,10 +47,6 @@ class SimpleTableCellBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -64,7 +60,6 @@ class SimpleTableCellBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart index 1a2e21c305..0987bc29d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart @@ -238,13 +238,8 @@ extension TableNodeExtension on Node { try { final columnWidths = parentTableNode.attributes[SimpleTableBlockKeys.columnWidths]; - final width = columnWidths?[columnIndex.toString()] as Object?; - if (width == null) { - return SimpleTableConstants.defaultColumnWidth; - } - return width.toDouble( - defaultValue: SimpleTableConstants.defaultColumnWidth, - ); + final width = columnWidths?[columnIndex.toString()]; + return width ?? SimpleTableConstants.defaultColumnWidth; } catch (e) { Log.warn('get column width: $e'); return SimpleTableConstants.defaultColumnWidth; @@ -861,18 +856,3 @@ extension TableNodeExtension on Node { return TableAlign.left; } } - -extension on Object { - double toDouble({double defaultValue = 0}) { - if (this is double) { - return this as double; - } - if (this is String) { - return double.tryParse(this as String) ?? defaultValue; - } - if (this is int) { - return (this as int).toDouble(); - } - return defaultValue; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart index 99f23d1ee9..d61d371e1b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart @@ -39,10 +39,6 @@ class SimpleTableRowBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -56,7 +52,6 @@ class SimpleTableRowBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.alwaysDistributeColumnWidths, }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart index bfc31e8abc..74bdc3fc62 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -41,9 +40,7 @@ KeyEventResult _enterInTableCellHandler(EditorState editorState) { return KeyEventResult.handled; } } - if (node.type != CalloutBlockKeys.type) { - return convertToParagraphCommand.execute(editorState); - } + return convertToParagraphCommand.execute(editorState); } return KeyEventResult.ignored; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart index 04a8ee1b7e..0bb09d0a7e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart @@ -46,12 +46,12 @@ extension on EditorState { selection.start.offset, 0, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: DateTime.now().toIso8601String(), - reminderId: null, - reminderOption: null, - includeTime: false, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: DateTime.now().toIso8601String(), + }, + }, ); await apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart index 890ba113cc..df1c457cc2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart @@ -1,13 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; -import 'package:appflowy/plugins/emoji/emoji_menu.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.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:universal_platform/universal_platform.dart'; import 'slash_menu_item_builder.dart'; @@ -40,16 +37,11 @@ extension on EditorState { }) async { final container = Overlay.of(context); menuService.dismiss(); - if (UniversalPlatform.isMobile || selection == null) { - return; - } - - final node = getNodeAtPath(selection!.end.path); - final delta = node?.delta; - if (node == null || delta == null || node.type == CodeBlockKeys.type) { - return; - } - emojiMenuService = EmojiMenu(editorState: this, overlay: container); - emojiMenuService?.show(''); + showEmojiPickerMenu( + container, + this, + menuService.alignment, + menuService.offset, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart index 844b73c3e1..f0ce852e41 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart @@ -17,20 +17,17 @@ final _keywords = [ ]; /// Image menu item -final imageSlashMenuItem = buildImageSlashMenuItem(); - -SelectionMenuItem buildImageSlashMenuItem({FlowySvgData? svg}) => - SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_image.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertImageBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: svg ?? FlowySvgs.slash_menu_icon_image_s, - isSelected: isSelected, - style: style, - ), - ); +final imageSlashMenuItem = SelectionMenuItem( + getName: () => LocaleKeys.document_slashMenu_name_image.tr(), + keywords: _keywords, + handler: (editorState, _, __) async => editorState.insertImageBlock(), + nameBuilder: slashMenuItemNameBuilder, + icon: (editorState, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.slash_menu_icon_image_s, + isSelected: isSelected, + style: style, + ), +); extension on EditorState { Future insertImageBlock() async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart index b71b54ad40..b69e74bd74 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart @@ -15,7 +15,7 @@ final List mobileItems = [ mobileTableSlashMenuItem, visualsMobileSlashMenuItem, dateOrReminderSlashMenuItem, - buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), + subPageSlashMenuItem, advancedMobileSlashMenuItem, ]; @@ -26,7 +26,7 @@ final List mobileItemsInTale = [ fileAndMediaMobileSlashMenuItem, visualsMobileSlashMenuItem, dateOrReminderSlashMenuItem, - buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), + subPageSlashMenuItem, advancedMobileSlashMenuItem, ]; @@ -93,7 +93,7 @@ MobileSelectionMenuItem fileAndMediaMobileSlashMenuItem = ), nameBuilder: slashMenuItemNameBuilder, children: [ - buildImageSlashMenuItem(svg: FlowySvgs.slash_menu_image_m), + imageSlashMenuItem, photoGallerySlashMenuItem, fileSlashMenuItem, ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart index 8609b76e70..a0966465a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart @@ -90,8 +90,13 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( ); Node _buildColumnsNode(EditorState editorState, int columnCount) { - return simpleColumnsNode( - columnCount: columnCount, - ratio: 1.0 / columnCount, - ); + final selection = editorState.selection; + double? width; + if (selection != null) { + final parentNode = editorState.getNodeAtPath(selection.start.path); + if (parentNode != null) { + width = parentNode.rect.width / columnCount - 16; + } + } + return simpleColumnsNode(columnCount: columnCount, width: width); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart index 1052dbbe3e..bc7f7e46b4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart @@ -22,29 +22,26 @@ final _keywords = [ ]; // Sub-page menu item -SelectionMenuItem subPageSlashMenuItem = buildSubpageSlashMenuItem(); - -SelectionMenuItem buildSubpageSlashMenuItem({FlowySvgData? svg}) => - SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(), - keywords: _keywords, - updateSelection: (editorState, path, __, ___) { - final context = editorState.document.root.context; - if (context != null) { - final isInDatabase = - context.read().isInDatabaseRowPage; - if (isInDatabase) { - Navigator.of(context).pop(); - } - } - return Selection.collapsed(Position(path: path)); - }, - replace: (_, node) => node.delta?.isEmpty ?? false, - nodeBuilder: (_, __) => subPageNode(), - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: svg ?? FlowySvgs.insert_document_s, - isSelected: isSelected, - style: style, - ), - ); +SelectionMenuItem subPageSlashMenuItem = SelectionMenuItem.node( + getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(), + keywords: _keywords, + updateSelection: (editorState, path, __, ___) { + final context = editorState.document.root.context; + if (context != null) { + final isInDatabase = + context.read().isInDatabaseRowPage; + if (isInDatabase) { + Navigator.of(context).pop(); + } + } + return Selection.collapsed(Position(path: path)); + }, + replace: (_, node) => node.delta?.isEmpty ?? false, + nodeBuilder: (_, __) => subPageNode(), + nameBuilder: slashMenuItemNameBuilder, + iconBuilder: (_, isSelected, style) => SelectableSvgWidget( + data: FlowySvgs.insert_document_s, + isSelected: isSelected, + style: style, + ), +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart index 518dccb35e..74ffa7e075 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart @@ -15,7 +15,7 @@ final _keywords = [ ]; final todoListSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.editor_checkbox.tr(), + getName: () => LocaleKeys.document_slashMenu_name_todoList.tr(), keywords: _keywords, handler: (editorState, _, __) async => insertCheckboxAfterSelection( editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart index 137f592902..e953694966 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart @@ -1,7 +1,5 @@ import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -15,16 +13,9 @@ List slashMenuItemsBuilder({ DocumentBloc? documentBloc, EditorState? editorState, Node? node, - ViewPB? view, }) { final isInTable = node != null && node.parentTableCellNode != null; final isMobile = UniversalPlatform.isMobile; - bool isEmpty = false; - if (editorState == null || editorState.isEmptyForContinueWriting()) { - if (view == null || view.name.isEmpty) { - isEmpty = true; - } - } if (isMobile) { if (isInTable) { return mobileItemsInTale; @@ -38,7 +29,6 @@ List slashMenuItemsBuilder({ return _defaultSlashMenuItems( isLocalMode: isLocalMode, documentBloc: documentBloc, - isEmpty: isEmpty, ); } } @@ -58,12 +48,11 @@ List slashMenuItemsBuilder({ List _defaultSlashMenuItems({ bool isLocalMode = false, DocumentBloc? documentBloc, - bool isEmpty = false, }) { return [ // ai if (!isLocalMode) ...[ - if (!isEmpty) continueWritingSlashMenuItem, + continueWritingSlashMenuItem, aiWriterSlashMenuItem, ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart index 0ce2b74a74..ad69d4c784 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart @@ -73,7 +73,6 @@ class SubPageBlockComponent extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -296,7 +295,6 @@ class SubPageBlockComponentState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 9e93f80ce4..9fc0a02cec 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -111,10 +111,6 @@ class ToggleListBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @@ -128,7 +124,6 @@ class ToggleListBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.padding = const EdgeInsets.all(0), this.textStyleBuilder, @@ -252,7 +247,6 @@ class _ToggleListBlockComponentWidgetState child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart index f3059bf1be..212be0f7bf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart @@ -47,12 +47,15 @@ Future _formatGreaterToToggleHeading( delta = delta.compose(Delta()..delete(_greater.length)); // if the previous block is heading block, convert it to toggle heading block if (type == HeadingBlockKeys.type && level != null) { - await BlockActionOptionCubit.turnIntoSingleToggleHeading( + final cubit = BlockActionOptionCubit( + editorState: editorState, + blockComponentBuilder: {}, + ); + await cubit.turnIntoSingleToggleHeading( type: ToggleListBlockKeys.type, selectedNodes: [node], level: level, delta: delta, - editorState: editorState, afterSelection: afterSelection, ); return; @@ -95,8 +98,7 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( } final slicedDelta = delta.slice(selection.start.offset); final transaction = editorState.transaction; - final bool collapsed = - node.attributes[ToggleListBlockKeys.collapsed] ?? false; + final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool; if (collapsed) { // if the delta is empty, clear the format if (delta.isEmpty) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart deleted file mode 100644 index d4f3d21f46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flutter/material.dart'; - -import 'custom_placeholder_toolbar_item.dart'; -import 'toolbar_id_enum.dart'; - -final List customMarkdownFormatItems = [ - _FormatToolbarItem( - id: ToolbarId.bold, - name: 'bold', - svg: FlowySvgs.toolbar_bold_m, - ), - group1PaddingItem, - _FormatToolbarItem( - id: ToolbarId.underline, - name: 'underline', - svg: FlowySvgs.toolbar_underline_m, - ), - group1PaddingItem, - _FormatToolbarItem( - id: ToolbarId.italic, - name: 'italic', - svg: FlowySvgs.toolbar_inline_italic_m, - ), -]; - -final ToolbarItem customInlineCodeItem = _FormatToolbarItem( - id: ToolbarId.code, - name: 'code', - svg: FlowySvgs.toolbar_inline_code_m, - group: 2, -); - -class _FormatToolbarItem extends ToolbarItem { - _FormatToolbarItem({ - required ToolbarId id, - required String name, - required FlowySvgData svg, - super.group = 1, - }) : super( - id: id.id, - isActive: showInAnyTextType, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection( - selection, - (delta) => - delta.isNotEmpty && - delta.everyAttributes((attr) => attr[name] == true), - ); - - final hoverColor = isHighlight - ? highlightColor - : EditorStyleCustomizer.toolbarHoverColor(context); - final isDark = !Theme.of(context).isLightMode; - final theme = AppFlowyTheme.of(context); - - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: hoverColor, - isSelected: isHighlight, - icon: FlowySvg( - svg, - size: Size.square(20.0), - color: (isDark && isHighlight) - ? Color(0xFF282E3A) - : theme.iconColorScheme.primary, - ), - onPressed: () => editorState.toggleAttribute( - name, - selection: selection, - ), - ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - id.id, - _getTooltipText(id), - child, - ); - } - return child; - }, - ); -} - -String _getTooltipText(ToolbarId id) { - switch (id) { - case ToolbarId.underline: - return '${LocaleKeys.toolbar_underline.tr()}${shortcutTooltips( - '⌘ + U', - 'CTRL + U', - 'CTRL + U', - )}'; - case ToolbarId.bold: - return '${LocaleKeys.toolbar_bold.tr()}${shortcutTooltips( - '⌘ + B', - 'CTRL + B', - 'CTRL + B', - )}'; - case ToolbarId.italic: - return '${LocaleKeys.toolbar_italic.tr()}${shortcutTooltips( - '⌘ + I', - 'CTRL + I', - 'CTRL + I', - )}'; - case ToolbarId.code: - return '${LocaleKeys.document_toolbar_inlineCode.tr()}${shortcutTooltips( - '⌘ + E', - 'CTRL + E', - 'CTRL + E', - )}'; - default: - return ''; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart deleted file mode 100644 index 46f2c02c5a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -String? _customHighlightColorHex; - -final customHighlightColorItem = ToolbarItem( - id: ToolbarId.highlightColor.id, - group: 1, - isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => - HighlightColorPickerWidget( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ), -); - -class HighlightColorPickerWidget extends StatefulWidget { - const HighlightColorPickerWidget({ - super.key, - required this.editorState, - this.tooltipBuilder, - required this.highlightColor, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => - _HighlightColorPickerWidgetState(); -} - -class _HighlightColorPickerWidgetState - extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - if (editorState.selection == null) { - return const SizedBox.shrink(); - } - final selectionRectList = editorState.selectionRects(); - final top = - selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, top), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - margin: EdgeInsets.zero, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: SizedBox( - width: 20, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_highlight_m, - size: Size(20, 16), - color: iconColor, - ), - buildColorfulDivider(iconColor), - ], - ), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.highlightColor.id, - AppFlowyEditorL10n.current.highlightColor, - child, - ) ?? - child; - } - - Widget buildColorfulDivider(Color? iconColor) { - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.backgroundColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - - final colorLength = colors.length; - if (colors.isEmpty || !isHighLight) { - return Container( - width: 20, - height: 4, - color: iconColor, - ); - } - return SizedBox( - width: 20, - height: 4, - child: Row( - children: List.generate(colorLength, (index) { - final currentColor = int.tryParse(colors[index]); - return Container( - width: 20 / colorLength, - height: 4, - color: currentColor == null ? iconColor : Color(currentColor), - ); - }), - ), - ); - } - - Widget buildPopoverContent() { - final List colors = []; - - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attributes) { - final highlightColorHex = - attributes[AppFlowyRichTextKeys.backgroundColor]; - if (highlightColorHex != null) colors.add(highlightColorHex); - return highlightColorHex != null; - }); - }); - bool showClearButton = false; - nodes.allSatisfyInSelection(selection, (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element.attributes?[AppFlowyRichTextKeys.backgroundColor] != - null; - }, - ); - } - return true; - }); - return MouseRegion( - child: ColorPicker( - title: AppFlowyEditorL10n.current.highlightColor, - showClearButton: showClearButton, - selectedColorHex: - (colors.length == 1 && isHighlight) ? colors.first : null, - customColorHex: _customHighlightColorHex, - colorOptions: generateHighlightColorOptions(), - onSubmittedColorHex: (color, isCustomColor) { - if (isCustomColor) { - _customHighlightColorHex = color; - } - formatHighlightColor( - editorState, - editorState.selection, - color, - withUpdateSelection: true, - ); - hidePopover(); - }, - resetText: AppFlowyEditorL10n.current.clearHighlightColor, - resetIconName: 'clear_highlight_color', - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void hidePopover() { - popoverController.close(); - keepEditorFocusNotifier.decrease(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart deleted file mode 100644 index 8c9e6b69da..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'toolbar_id_enum.dart'; - -const kIsPageLink = 'is_page_link'; - -final customLinkItem = ToolbarItem( - id: ToolbarId.link.id, - group: 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHref = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[AppFlowyRichTextKeys.href] != null, - ); - }); - - final isDark = !Theme.of(context).isLightMode; - final hoverColor = isHref - ? highlightColor - : EditorStyleCustomizer.toolbarHoverColor(context); - final theme = AppFlowyTheme.of(context); - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: hoverColor, - isSelected: isHref, - icon: FlowySvg( - FlowySvgs.toolbar_link_m, - size: Size.square(20.0), - color: (isDark && isHref) - ? Color(0xFF282E3A) - : theme.iconColorScheme.primary, - ), - onPressed: () { - getIt().hideToolbar(); - if (!isHref) { - final viewId = context.read()?.documentId ?? ''; - showLinkCreateMenu(context, editorState, selection, viewId); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - getIt() - .call(HoverTriggerKey(nodes.first.id, selection)); - }); - } - }, - ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - ToolbarId.highlightColor.id, - AppFlowyEditorL10n.current.link, - child, - ); - } - - return child; - }, -); - -extension AttributeExtension on Attributes { - bool get isPage { - if (this[kIsPageLink] is bool) { - return this[kIsPageLink]; - } - return false; - } -} - -enum LinkMenuAlignment { - topLeft, - topRight, - bottomLeft, - bottomRight, -} - -extension LinkMenuAlignmentExtension on LinkMenuAlignment { - bool get isTop => - this == LinkMenuAlignment.topLeft || this == LinkMenuAlignment.topRight; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart deleted file mode 100644 index e087731c82..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customPlaceholderItem = ToolbarItem( - id: ToolbarId.placeholder.id, - group: -1, - isActive: (editorState) => true, - builder: (context, __, ___, ____, _____) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5), - child: Container( - width: 1, - color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 255), - ), - ); - }, -); - -ToolbarItem buildPaddingPlaceholderItem( - int group, { - bool Function(EditorState editorState)? isActive, -}) => - ToolbarItem( - id: ToolbarId.paddingPlaceHolder.id, - group: group, - isActive: isActive, - builder: (context, __, ___, ____, _____) => HSpace(4), - ); - -ToolbarItem group0PaddingItem = buildPaddingPlaceholderItem( - 0, - isActive: onlyShowInTextTypeAndExcludeTable, -); - -ToolbarItem group1PaddingItem = - buildPaddingPlaceholderItem(1, isActive: showInAnyTextType); - -ToolbarItem group4PaddingItem = buildPaddingPlaceholderItem( - 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart deleted file mode 100644 index efaff532f4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customTextAlignItem = ToolbarItem( - id: ToolbarId.textAlign.id, - group: 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return TextAlignActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ); - }, -); - -class TextAlignActionList extends StatefulWidget { - const TextAlignActionList({ - super.key, - required this.editorState, - required this.highlightColor, - this.tooltipBuilder, - this.child, - this.onSelect, - this.popoverController, - this.popoverDirection = PopoverDirection.bottomWithLeftAligned, - this.showOffset = const Offset(0, 2), - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - final Widget? child; - final VoidCallback? onSelect; - final PopoverController? popoverController; - final PopoverDirection popoverDirection; - final Offset showOffset; - - @override - State createState() => _TextAlignActionListState(); -} - -class _TextAlignActionListState extends State { - late PopoverController popoverController = - widget.popoverController ?? PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: widget.popoverDirection, - offset: widget.showOffset, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: widget.child ?? buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_alignment_m, - size: Size.square(20), - color: iconColor, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textAlign.id, - LocaleKeys.document_toolbar_textAlign.tr(), - child, - ) ?? - child; - } - - Widget buildPopoverContent() { - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: List.generate(TextAlignCommand.values.length, (index) { - final command = TextAlignCommand.values[index]; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.every( - (n) => n.attributes[blockComponentAlign] == command.name, - ); - - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - iconPadding: 12, - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: - isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () { - command.onAlignChanged(editorState); - widget.onSelect?.call(); - popoverController.close(); - }, - ), - ); - }), - ), - ); - } -} - -enum TextAlignCommand { - left(FlowySvgs.toolbar_text_align_left_m), - center(FlowySvgs.toolbar_text_align_center_m), - right(FlowySvgs.toolbar_text_align_right_m); - - const TextAlignCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case left: - return LocaleKeys.document_toolbar_alignLeft.tr(); - case center: - return LocaleKeys.document_toolbar_alignCenter.tr(); - case right: - return LocaleKeys.document_toolbar_alignRight.tr(); - } - } - - Future onAlignChanged(EditorState editorState) async { - final selection = editorState.selection!; - - await editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: name, - }, - ), - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart deleted file mode 100644 index 9f5a917b89..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'toolbar_id_enum.dart'; - -String? _customColorHex; - -final customTextColorItem = ToolbarItem( - id: ToolbarId.textColor.id, - group: 1, - isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => - TextColorPickerWidget( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ), -); - -class TextColorPickerWidget extends StatefulWidget { - const TextColorPickerWidget({ - super.key, - required this.editorState, - this.tooltipBuilder, - required this.highlightColor, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => _TextColorPickerWidgetState(); -} - -class _TextColorPickerWidgetState extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - if (editorState.selection == null) { - return const SizedBox.shrink(); - } - final selectionRectList = editorState.selectionRects(); - final top = - selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, top), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - margin: EdgeInsets.zero, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: SizedBox( - width: 20, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_color_m, - size: Size(20, 16), - color: iconColor, - ), - buildColorfulDivider(iconColor), - ], - ), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textColor.id, - LocaleKeys.document_toolbar_textColor.tr(), - child, - ) ?? - child; - } - - Widget buildColorfulDivider(Color? iconColor) { - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.textColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - - final colorLength = colors.length; - if (colors.isEmpty || !isHighLight) { - return Container( - width: 20, - height: 4, - color: iconColor, - ); - } - return SizedBox( - width: 20, - height: 4, - child: Row( - children: List.generate(colorLength, (index) { - final currentColor = int.tryParse(colors[index]); - return Container( - width: 20 / colorLength, - height: 4, - color: currentColor == null ? iconColor : Color(currentColor), - ); - }), - ), - ); - } - - Widget buildPopoverContent() { - bool showClearButton = false; - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.textColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - nodes.allSatisfyInSelection( - selection, - (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element.attributes?[AppFlowyRichTextKeys.textColor] != - null; - }, - ); - } - return true; - }, - ); - return MouseRegion( - child: ColorPicker( - title: LocaleKeys.document_toolbar_textColor.tr(), - showClearButton: showClearButton, - selectedColorHex: - (colors.length == 1 && isHighLight) ? colors.first : null, - customColorHex: _customColorHex, - colorOptions: generateTextColorOptions(), - onSubmittedColorHex: (color, isCustomColor) { - if (isCustomColor) { - _customColorHex = color; - } - formatFontColor( - editorState, - editorState.selection, - color, - withUpdateSelection: true, - ); - hidePopover(); - }, - resetText: AppFlowyEditorL10n.current.resetToDefaultColor, - resetIconName: 'reset_text_color', - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void hidePopover() { - popoverController.close(); - keepEditorFocusNotifier.decrease(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart deleted file mode 100644 index 46b707a8d3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ /dev/null @@ -1,455 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'custom_text_align_toolbar_item.dart'; -import 'text_suggestions_toolbar_item.dart'; - -const _kMoreOptionItemId = 'editor.more_option'; -const kFontToolbarItemId = 'editor.font'; - -@visibleForTesting -const kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem'); - -final ToolbarItem moreOptionItem = ToolbarItem( - id: _kMoreOptionItemId, - group: 5, - isActive: showInAnyTextType, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return MoreOptionActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ); - }, -); - -class MoreOptionActionList extends StatefulWidget { - const MoreOptionActionList({ - super.key, - required this.editorState, - required this.highlightColor, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => _MoreOptionActionListState(); -} - -class _MoreOptionActionListState extends State { - final popoverController = PopoverController(); - PopoverController fontPopoverController = PopoverController(); - PopoverController suggestionsPopoverController = PopoverController(); - PopoverController textAlignPopoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - MoreOptionCommand? tappedCommand; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - fontPopoverController.close(); - suggestionsPopoverController.close(); - textAlignPopoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; - final child = FlowyIconButton( - width: 36, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - size: Size.square(20), - color: iconColor, - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - _kMoreOptionItemId, - LocaleKeys.document_toolbar_moreOptions.tr(), - child, - ) ?? - child; - } - - Color? getFormulaColor() { - if (isFormulaHighlight(editorState)) { - return widget.highlightColor; - } - return null; - } - - Color? getStrikethroughColor() { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return null; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection( - selection, - (delta) => - delta.isNotEmpty && - delta.everyAttributes( - (attr) => attr[MoreOptionCommand.strikethrough.name] == true, - ), - ); - return isHighlight ? widget.highlightColor : null; - } - - Widget buildPopoverContent() { - final showFormula = onlyShowInSingleSelectionAndTextType(editorState); - const fontColor = Color(0xff99A1A8); - final isNarrow = isNarrowWindow(editorState); - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: [ - if (isNarrow) ...[ - buildTurnIntoSelector(), - buildCommandItem(MoreOptionCommand.link), - buildTextAlignSelector(), - ], - buildFontSelector(), - buildCommandItem( - MoreOptionCommand.strikethrough, - rightIcon: FlowyText( - shortcutTooltips( - '⌘⇧S', - 'Ctrl⇧S', - 'Ctrl⇧S', - ).trim(), - color: fontColor, - fontSize: 12, - figmaLineHeight: 16, - fontWeight: FontWeight.w400, - ), - ), - if (showFormula) - buildCommandItem( - MoreOptionCommand.formula, - rightIcon: FlowyText( - shortcutTooltips( - '⌘⇧E', - 'Ctrl⇧E', - 'Ctrl⇧E', - ).trim(), - color: fontColor, - fontSize: 12, - figmaLineHeight: 16, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ); - } - - Widget buildCommandItem( - MoreOptionCommand command, { - Widget? rightIcon, - VoidCallback? onTap, - }) { - final isFontCommand = command == MoreOptionCommand.font; - return SizedBox( - height: 36, - child: FlowyButton( - key: isFontCommand ? kFontFamilyToolbarItemKey : null, - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - rightIcon: rightIcon, - iconPadding: 12, - text: FlowyText( - command.title, - figmaLineHeight: 20, - fontWeight: FontWeight.w400, - ), - onTap: onTap ?? - () { - command.onExecute(editorState, context); - hideOtherPopovers(command); - if (command != MoreOptionCommand.font) { - popoverController.close(); - } - }, - ), - ); - } - - Widget buildFontSelector() { - final selection = editorState.selection!; - final String? currentFontFamily = editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); - return FontFamilyDropDown( - currentFontFamily: currentFontFamily ?? '', - offset: const Offset(-240, 0), - popoverController: fontPopoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - onFontFamilyChanged: (fontFamily) async { - fontPopoverController.close(); - popoverController.close(); - try { - await editorState.formatDelta(selection, { - AppFlowyRichTextKeys.fontFamily: fontFamily, - }); - } catch (e) { - Log.error('Failed to set font family: $e'); - } - }, - onResetFont: () async { - fontPopoverController.close(); - popoverController.close(); - await editorState - .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); - }, - child: buildCommandItem( - MoreOptionCommand.font, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - ), - ); - } - - Widget buildTurnIntoSelector() { - final selectionRects = editorState.selectionRects(); - double height = -6; - if (selectionRects.isNotEmpty) height = selectionRects.first.height; - return SuggestionsActionList( - editorState: editorState, - popoverController: suggestionsPopoverController, - popoverDirection: PopoverDirection.leftWithTopAligned, - showOffset: Offset(-8, height), - onSelect: () => getIt().hideToolbar(), - child: buildCommandItem( - MoreOptionCommand.suggestions, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - onTap: () { - if (tappedCommand == MoreOptionCommand.suggestions) return; - hideOtherPopovers(MoreOptionCommand.suggestions); - keepEditorFocusNotifier.increase(); - suggestionsPopoverController.show(); - }, - ), - ); - } - - Widget buildTextAlignSelector() { - return TextAlignActionList( - editorState: editorState, - popoverController: textAlignPopoverController, - popoverDirection: PopoverDirection.leftWithTopAligned, - showOffset: Offset(-8, 0), - onSelect: () => getIt().hideToolbar(), - highlightColor: highlightColor, - child: buildCommandItem( - MoreOptionCommand.textAlign, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - onTap: () { - if (tappedCommand == MoreOptionCommand.textAlign) return; - hideOtherPopovers(MoreOptionCommand.textAlign); - keepEditorFocusNotifier.increase(); - textAlignPopoverController.show(); - }, - ), - ); - } - - void hideOtherPopovers(MoreOptionCommand currentCommand) { - if (tappedCommand == currentCommand) return; - if (tappedCommand == MoreOptionCommand.font) { - fontPopoverController.close(); - fontPopoverController = PopoverController(); - } else if (tappedCommand == MoreOptionCommand.suggestions) { - suggestionsPopoverController.close(); - suggestionsPopoverController = PopoverController(); - } else if (tappedCommand == MoreOptionCommand.textAlign) { - textAlignPopoverController.close(); - textAlignPopoverController = PopoverController(); - } - tappedCommand = currentCommand; - } -} - -enum MoreOptionCommand { - suggestions(FlowySvgs.turninto_s), - link(FlowySvgs.toolbar_link_m), - textAlign( - FlowySvgs.toolbar_alignment_m, - ), - font(FlowySvgs.type_font_m), - strikethrough(FlowySvgs.type_strikethrough_m), - formula(FlowySvgs.type_formula_m); - - const MoreOptionCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case suggestions: - return LocaleKeys.document_toolbar_turnInto.tr(); - case link: - return LocaleKeys.document_toolbar_link.tr(); - case textAlign: - return LocaleKeys.button_align.tr(); - case font: - return LocaleKeys.document_toolbar_font.tr(); - case strikethrough: - return LocaleKeys.editor_strikethrough.tr(); - case formula: - return LocaleKeys.document_toolbar_equation.tr(); - } - } - - Future onExecute(EditorState editorState, BuildContext context) async { - final selection = editorState.selection!; - if (this == link) { - final nodes = editorState.getNodesInSelection(selection); - final isHref = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[AppFlowyRichTextKeys.href] != null, - ); - }); - getIt().hideToolbar(); - if (isHref) { - getIt().call( - HoverTriggerKey(nodes.first.id, selection), - ); - } else { - final viewId = context.read()?.documentId ?? ''; - showLinkCreateMenu(context, editorState, selection, viewId); - } - } else if (this == strikethrough) { - await editorState.toggleAttribute(name); - } else if (this == formula) { - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - final transaction = editorState.transaction; - final isHighlight = isFormulaHighlight(editorState); - if (isHighlight) { - final formula = delta - .slice(selection.startIndex, selection.endIndex) - .whereType() - .firstOrNull - ?.attributes?[InlineMathEquationKeys.formula]; - assert(formula != null); - if (formula == null) { - return; - } - // clear the format - transaction.replaceText( - node, - selection.startIndex, - selection.length, - formula, - attributes: {}, - ); - } else { - final text = editorState.getTextInSelection(selection).join(); - transaction.replaceText( - node, - selection.startIndex, - selection.length, - MentionBlockKeys.mentionChar, - attributes: { - InlineMathEquationKeys.formula: text, - }, - ); - } - await editorState.apply(transaction); - } - } -} - -bool isFormulaHighlight(EditorState editorState) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return false; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return false; - } - - final nodes = editorState.getNodesInSelection(selection); - return nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[InlineMathEquationKeys.formula] != null, - ); - }); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart deleted file mode 100644 index 5778b6b8a4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customTextHeadingItem = ToolbarItem( - id: ToolbarId.textHeading.id, - group: 1, - isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return TextHeadingActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ); - }, -); - -class TextHeadingActionList extends StatefulWidget { - const TextHeadingActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - - @override - State createState() => _TextHeadingActionListState(); -} - -class _TextHeadingActionListState extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_format_m, - size: Size.square(20), - color: iconColor, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textHeading.id, - LocaleKeys.document_toolbar_textSize.tr(), - child, - ) ?? - child; - } - - Widget buildPopoverContent() { - final selectingCommand = getSelectingCommand(); - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: List.generate(TextHeadingCommand.values.length, (index) { - final command = TextHeadingCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - iconPadding: 12, - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: selectingCommand == command - ? FlowySvg(FlowySvgs.toolbar_check_m) - : null, - onTap: () { - if (command == selectingCommand) return; - command.onExecute(widget.editorState); - popoverController.close(); - }, - ), - ); - }), - ), - ); - } - - TextHeadingCommand? getSelectingCommand() { - final editorState = widget.editorState; - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return null; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.delta == null) { - return null; - } - final nodeType = node.type; - if (nodeType == ParagraphBlockKeys.type) return TextHeadingCommand.text; - if (nodeType == HeadingBlockKeys.type) { - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level == 1) return TextHeadingCommand.h1; - if (level == 2) return TextHeadingCommand.h2; - if (level == 3) return TextHeadingCommand.h3; - } - return null; - } -} - -enum TextHeadingCommand { - text(FlowySvgs.type_text_m), - h1(FlowySvgs.type_h1_m), - h2(FlowySvgs.type_h2_m), - h3(FlowySvgs.type_h3_m); - - const TextHeadingCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case text: - return AppFlowyEditorL10n.current.text; - case h1: - return LocaleKeys.document_toolbar_h1.tr(); - case h2: - return LocaleKeys.document_toolbar_h2.tr(); - case h3: - return LocaleKeys.document_toolbar_h3.tr(); - } - } - - void onExecute(EditorState state) { - switch (this) { - case text: - formatNodeToText(state); - break; - case h1: - _turnInto(state, 1); - break; - case h2: - _turnInto(state, 2); - break; - case h3: - _turnInto(state, 3); - break; - } - } - - Future _turnInto(EditorState state, int level) async { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - await BlockActionOptionCubit.turnIntoBlock( - HeadingBlockKeys.type, - node, - state, - level: level, - keepSelection: true, - ); - } -} - -void formatNodeToText(EditorState editorState) { - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!; - final delta = (node.delta ?? Delta()).toJson(); - editorState.formatNode( - selection, - (node) => node.copyWith( - type: ParagraphBlockKeys.type, - attributes: { - blockComponentDelta: delta, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - }, - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart deleted file mode 100644 index 48f5d3f403..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ /dev/null @@ -1,536 +0,0 @@ -import 'dart:collection'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import 'text_heading_toolbar_item.dart'; -import 'toolbar_id_enum.dart'; - -@visibleForTesting -const kSuggestionsItemKey = ValueKey('SuggestionsItem'); - -@visibleForTesting -const kSuggestionsItemListKey = ValueKey('SuggestionsItemList'); - -final ToolbarItem suggestionsItem = ToolbarItem( - id: ToolbarId.suggestions.id, - group: 3, - isActive: enableSuggestions, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return SuggestionsActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ); - }, -); - -class SuggestionsActionList extends StatefulWidget { - const SuggestionsActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - this.child, - this.onSelect, - this.popoverController, - this.popoverDirection = PopoverDirection.bottomWithLeftAligned, - this.showOffset = const Offset(0, 2), - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Widget? child; - final VoidCallback? onSelect; - final PopoverController? popoverController; - final PopoverDirection popoverDirection; - final Offset showOffset; - - @override - State createState() => _SuggestionsActionListState(); -} - -class _SuggestionsActionListState extends State { - late PopoverController popoverController = - widget.popoverController ?? PopoverController(); - - bool isSelected = false; - - final List suggestionItems = suggestions.sublist(0, 4); - final List turnIntoItems = - suggestions.sublist(4, suggestions.length); - - EditorState get editorState => widget.editorState; - - SuggestionItem currentSuggestionItem = textSuggestionItem; - - @override - void initState() { - super.initState(); - refreshSuggestions(); - editorState.selectionNotifier.addListener(refreshSuggestions); - } - - @override - void dispose() { - editorState.selectionNotifier.removeListener(refreshSuggestions); - popoverController.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: widget.popoverDirection, - offset: widget.showOffset, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400), - popupBuilder: (context) => buildPopoverContent(context), - child: widget.child ?? buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyHover( - isSelected: () => isSelected, - style: HoverStyle( - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - foregroundColorOnHover: Theme.of(context).iconTheme.color, - ), - resetHoverOnRebuild: false, - child: FlowyTooltip( - preferBelow: true, - child: RawMaterialButton( - key: kSuggestionsItemKey, - constraints: BoxConstraints(maxHeight: 32, minWidth: 60), - clipBehavior: Clip.antiAlias, - hoverElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder(borderRadius: Corners.s6Border), - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - currentSuggestionItem.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - ), - ), - ), - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.suggestions.id, - currentSuggestionItem.title, - child, - ) ?? - child; - } - - Widget buildPopoverContent(BuildContext context) { - final textColor = Color(0xff99A1A8); - return MouseRegion( - child: SingleChildScrollView( - key: kSuggestionsItemListKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildSubTitle( - LocaleKeys.document_toolbar_suggestions.tr(), - textColor, - ), - ...List.generate(suggestionItems.length, (index) { - return buildItem(suggestionItems[index]); - }), - buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor), - ...List.generate(turnIntoItems.length, (index) { - return buildItem(turnIntoItems[index]); - }), - ], - ), - ), - ); - } - - Widget buildItem(SuggestionItem item) { - 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(widget.editorState, true); - widget.onSelect?.call(); - popoverController.close(); - }, - ), - ); - } - - Widget buildSubTitle(String text, Color color) { - return Container( - height: 32, - margin: EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.semibold( - text, - color: color, - figmaLineHeight: 16, - ), - ), - ); - } - - void refreshSuggestions() { - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.delta == null) { - return; - } - final nodeType = node.type; - SuggestionType? suggestionType; - if (nodeType == HeadingBlockKeys.type) { - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level == 1) { - suggestionType = SuggestionType.h1; - } else if (level == 2) { - suggestionType = SuggestionType.h2; - } else if (level == 3) { - suggestionType = SuggestionType.h3; - } - } else if (nodeType == ToggleListBlockKeys.type) { - final level = node.attributes[ToggleListBlockKeys.level]; - if (level == null) { - suggestionType = SuggestionType.toggle; - } else if (level == 1) { - suggestionType = SuggestionType.toggleH1; - } else if (level == 2) { - suggestionType = SuggestionType.toggleH2; - } else if (level == 3) { - suggestionType = SuggestionType.toggleH3; - } - } else { - suggestionType = nodeType2SuggestionType[nodeType]; - } - if (suggestionType == null) return; - suggestionItems.clear(); - turnIntoItems.clear(); - for (final item in suggestions) { - if (item.type.group == suggestionType.group && - item.type != suggestionType) { - suggestionItems.add(item); - } else { - turnIntoItems.add(item); - } - } - currentSuggestionItem = - suggestions.where((item) => item.type == suggestionType).first; - if (mounted) setState(() {}); - } -} - -class SuggestionItem { - SuggestionItem({ - required this.type, - required this.title, - required this.svg, - required this.onTap, - }); - - final SuggestionType type; - final String title; - final FlowySvgData svg; - final Function(EditorState state, bool keepSelection) onTap; -} - -enum SuggestionGroup { textHeading, list, toggle, quote, page } - -enum SuggestionType { - text(SuggestionGroup.textHeading), - h1(SuggestionGroup.textHeading), - h2(SuggestionGroup.textHeading), - h3(SuggestionGroup.textHeading), - checkbox(SuggestionGroup.list), - bulleted(SuggestionGroup.list), - numbered(SuggestionGroup.list), - toggle(SuggestionGroup.toggle), - toggleH1(SuggestionGroup.toggle), - toggleH2(SuggestionGroup.toggle), - toggleH3(SuggestionGroup.toggle), - callOut(SuggestionGroup.quote), - quote(SuggestionGroup.quote), - page(SuggestionGroup.page); - - const SuggestionType(this.group); - - final SuggestionGroup group; -} - -final textSuggestionItem = SuggestionItem( - type: SuggestionType.text, - title: AppFlowyEditorL10n.current.text, - svg: FlowySvgs.type_text_m, - onTap: (state, _) => formatNodeToText(state), -); - -final h1SuggestionItem = SuggestionItem( - type: SuggestionType.h1, - title: LocaleKeys.document_toolbar_h1.tr(), - svg: FlowySvgs.type_h1_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 1, - keepSelection: keepSelection, - ), -); - -final h2SuggestionItem = SuggestionItem( - type: SuggestionType.h2, - title: LocaleKeys.document_toolbar_h2.tr(), - svg: FlowySvgs.type_h2_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 2, - keepSelection: keepSelection, - ), -); - -final h3SuggestionItem = SuggestionItem( - type: SuggestionType.h3, - title: LocaleKeys.document_toolbar_h3.tr(), - svg: FlowySvgs.type_h3_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 3, - keepSelection: keepSelection, - ), -); - -final checkboxSuggestionItem = SuggestionItem( - type: SuggestionType.checkbox, - title: LocaleKeys.editor_checkbox.tr(), - svg: FlowySvgs.type_todo_m, - onTap: (state, keepSelection) => _turnInto( - state, - TodoListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final bulletedSuggestionItem = SuggestionItem( - type: SuggestionType.bulleted, - title: LocaleKeys.editor_bulletedListShortForm.tr(), - svg: FlowySvgs.type_bulleted_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - BulletedListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final numberedSuggestionItem = SuggestionItem( - type: SuggestionType.numbered, - title: LocaleKeys.editor_numberedListShortForm.tr(), - svg: FlowySvgs.type_numbered_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - NumberedListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final toggleSuggestionItem = SuggestionItem( - type: SuggestionType.toggle, - title: LocaleKeys.editor_toggleListShortForm.tr(), - svg: FlowySvgs.type_toggle_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final toggleH1SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH1, - title: LocaleKeys.editor_toggleHeading1ShortForm.tr(), - svg: FlowySvgs.type_toggle_h1_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 1, - keepSelection: keepSelection, - ), -); - -final toggleH2SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH2, - title: LocaleKeys.editor_toggleHeading2ShortForm.tr(), - svg: FlowySvgs.type_toggle_h2_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 2, - keepSelection: keepSelection, - ), -); - -final toggleH3SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH3, - title: LocaleKeys.editor_toggleHeading3ShortForm.tr(), - svg: FlowySvgs.type_toggle_h3_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 3, - keepSelection: keepSelection, - ), -); - -final callOutSuggestionItem = SuggestionItem( - type: SuggestionType.callOut, - title: LocaleKeys.document_plugins_callout.tr(), - svg: FlowySvgs.type_callout_m, - onTap: (state, keepSelection) => _turnInto( - state, - CalloutBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final quoteSuggestionItem = SuggestionItem( - type: SuggestionType.quote, - title: LocaleKeys.editor_quote.tr(), - svg: FlowySvgs.type_quote_m, - onTap: (state, keepSelection) => _turnInto( - state, - QuoteBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final pateItem = SuggestionItem( - type: SuggestionType.page, - title: LocaleKeys.editor_page.tr(), - svg: FlowySvgs.icon_document_s, - onTap: (state, keepSelection) => _turnInto( - state, - SubPageBlockKeys.type, - viewId: getIt().latestOpenView?.id, - keepSelection: keepSelection, - ), -); - -Future _turnInto( - EditorState state, - String type, { - int? level, - String? viewId, - bool keepSelection = true, -}) async { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - await BlockActionOptionCubit.turnIntoBlock( - type, - node, - state, - level: level, - currentViewId: viewId, - keepSelection: keepSelection, - ); -} - -final suggestions = UnmodifiableListView([ - textSuggestionItem, - h1SuggestionItem, - h2SuggestionItem, - h3SuggestionItem, - checkboxSuggestionItem, - bulletedSuggestionItem, - numberedSuggestionItem, - toggleSuggestionItem, - toggleH1SuggestionItem, - toggleH2SuggestionItem, - toggleH3SuggestionItem, - callOutSuggestionItem, - quoteSuggestionItem, - pateItem, -]); - -final nodeType2SuggestionType = UnmodifiableMapView({ - ParagraphBlockKeys.type: SuggestionType.text, - NumberedListBlockKeys.type: SuggestionType.numbered, - BulletedListBlockKeys.type: SuggestionType.bulleted, - QuoteBlockKeys.type: SuggestionType.quote, - TodoListBlockKeys.type: SuggestionType.checkbox, - CalloutBlockKeys.type: SuggestionType.callOut, -}); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart deleted file mode 100644 index 8a97bb6648..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart +++ /dev/null @@ -1,19 +0,0 @@ -enum ToolbarId { - bold, - underline, - italic, - code, - highlightColor, - textColor, - link, - placeholder, - paddingPlaceHolder, - textAlign, - moreOption, - textHeading, - suggestions, -} - -extension ToolbarIdExtension on ToolbarId { - String get id => 'editor.$name'; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart index 36ea3d2704..9ea6477969 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart @@ -1,8 +1,6 @@ import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; /// Undo /// @@ -16,15 +14,10 @@ final CommandShortcutEvent customUndoCommand = CommandShortcutEvent( command: 'ctrl+z', macOSCommand: 'cmd+z', handler: (editorState) { - final context = editorState.document.root.context; - if (context == null) { + // if the selection is null, it means the keyboard service is disabled + if (editorState.selection == null) { return KeyEventResult.ignored; } - final editorContext = context.read(); - if (editorContext.coverTitleFocusNode.hasFocus) { - return KeyEventResult.ignored; - } - EditorNotification.undo().post(); return KeyEventResult.handled; }, @@ -42,15 +35,9 @@ final CommandShortcutEvent customRedoCommand = CommandShortcutEvent( command: 'ctrl+y,ctrl+shift+z', macOSCommand: 'cmd+shift+z', handler: (editorState) { - final context = editorState.document.root.context; - if (context == null) { + if (editorState.selection == null) { return KeyEventResult.ignored; } - final editorContext = context.read(); - if (editorContext.coverTitleFocusNode.hasFocus) { - return KeyEventResult.ignored; - } - EditorNotification.redo().post(); return KeyEventResult.handled; }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index cd9d7bb5e8..10a5e117af 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -28,9 +28,6 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:universal_platform/universal_platform.dart'; -import 'editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; - class EditorStyleCustomizer { EditorStyleCustomizer({ required this.context, @@ -62,12 +59,6 @@ class EditorStyleCustomizer { static double get optionMenuWidth => UniversalPlatform.isMobile ? 0 : 44; - static Color? toolbarHoverColor(BuildContext context) { - return Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).colorScheme.secondary - : AFThemeExtension.of(context).toolbarHoverColor; - } - EditorStyle style() { if (UniversalPlatform.isDesktopOrWeb) { return desktop(); @@ -135,7 +126,6 @@ class EditorStyleCustomizer { textSpanDecorator: customizeAttributeDecorator, textScaleFactor: context.watch().state.textScaleFactor, - textSpanOverlayBuilder: _buildTextSpanOverlay, ); } @@ -315,13 +305,14 @@ class EditorStyleCustomizer { ); } - TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { - if (fontFamily == null) { - return TextStyle(fontWeight: fontWeight); - } else if (fontFamily == defaultFontFamily) { - return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight); - } + FloatingToolbarStyle floatingToolbarStyleBuilder() => FloatingToolbarStyle( + backgroundColor: Theme.of(context).colorScheme.onTertiary, + ); + TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { + if (fontFamily == null || fontFamily == defaultFontFamily) { + return TextStyle(fontWeight: fontWeight); + } try { return getGoogleFontSafely(fontFamily, fontWeight: fontWeight); } on Exception { @@ -473,15 +464,14 @@ class EditorStyleCustomizer { ); } - if (href != null) { - return TextSpan( - style: before.style, - text: text.text, - mouseCursor: SystemMouseCursors.click, - ); - } else { - return before; - } + return defaultTextSpanDecoratorForAttribute( + context, + node, + index, + text, + before, + after, + ); } Widget buildToolbarItemTooltip( @@ -494,7 +484,7 @@ class EditorStyleCustomizer { child = FlowyTooltip( richMessage: tooltipMessage, preferBelow: false, - verticalOffset: 24, + verticalOffset: 20, child: child, ); @@ -506,7 +496,7 @@ class EditorStyleCustomizer { if (!toolbarItemsWithoutHover.contains(id)) { child = Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 4.0), child: FlowyHover( style: HoverStyle( hoverColor: Colors.grey.withValues(alpha: 0.3), @@ -556,10 +546,10 @@ class EditorStyleCustomizer { style: context.tooltipTextStyle(), ), TextSpan( - text: (Platform.isMacOS ? '⌘+' : 'Ctrl+') + tooltip.$2, - style: context.tooltipTextStyle()?.copyWith( - color: Theme.of(context).hintColor, - ), + text: (Platform.isMacOS ? '⌘+' : 'Ctrl+\\') + tooltip.$2, + style: context + .tooltipTextStyle() + ?.copyWith(color: Theme.of(context).hintColor), ), ], ); @@ -571,6 +561,7 @@ class EditorStyleCustomizer { if (style == null) { return null; } + final fontSize = style.fontSize ?? 14.0; final isLight = Theme.of(context).isLightMode; final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4); final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4); @@ -580,61 +571,19 @@ class EditorStyleCustomizer { decoration: TextDecoration.lineThrough, ), AiWriterBlockKeys.suggestionReplacement => style.copyWith( - color: textColor, + color: Colors.transparent, decoration: TextDecoration.underline, decorationColor: underlineColor, decorationThickness: 1.0, + // hack: https://jtmuller5.medium.com/the-ultimate-guide-to-underlining-text-in-flutter-57936f5c79bb + shadows: [ + Shadow( + color: textColor, + offset: Offset(0, -fontSize * 0.2), + ), + ], ), _ => style, }; } - - List _buildTextSpanOverlay( - BuildContext context, - Node node, - SelectableMixin delegate, - ) { - if (UniversalPlatform.isMobile) return []; - final delta = node.delta; - if (delta == null) return []; - final widgets = []; - final textInserts = delta.whereType(); - int index = 0; - final editorState = context.read(); - for (final textInsert in textInserts) { - if (textInsert.attributes?.href != null) { - final nodeSelection = Selection( - start: Position(path: node.path, offset: index), - end: Position( - path: node.path, - offset: index + textInsert.length, - ), - ); - final rectList = delegate.getRectsInSelection(nodeSelection); - if (rectList.isNotEmpty) { - for (final rect in rectList) { - widgets.add( - Positioned( - left: rect.left, - top: rect.top, - child: SizedBox( - width: rect.width, - height: rect.height, - child: LinkHoverTrigger( - editorState: editorState, - selection: nodeSelection, - attribute: textInsert.attributes!, - node: node, - size: rect.size, - ), - ), - ), - ); - } - } - } - index += textInsert.length; - } - return widgets; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart deleted file mode 100644 index c116680c2e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'emoji_menu.dart'; - -const _emojiCharacter = ':'; -final _letterRegExp = RegExp(r'^[a-zA-Z]$'); - -CharacterShortcutEvent emojiCommand(BuildContext context) => - CharacterShortcutEvent( - key: 'Opens Emoji Menu', - character: '', - regExp: _letterRegExp, - handler: (editorState) async { - return false; - }, - handlerWithCharacter: (editorState, character) { - emojiMenuService = EmojiMenu( - overlay: Overlay.of(context), - editorState: editorState, - ); - return emojiCommandHandler(editorState, context, character); - }, - ); - -EmojiMenuService? emojiMenuService; - -Future emojiCommandHandler( - EditorState editorState, - BuildContext context, - String character, -) async { - final selection = editorState.selection; - - if (UniversalPlatform.isMobile || selection == null) { - return false; - } - - final node = editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null || node.type == CodeBlockKeys.type) { - return false; - } - - if (selection.end.offset > 0) { - final plain = delta.toPlainText(); - - final previousCharacter = plain[selection.end.offset - 1]; - if (previousCharacter != _emojiCharacter) return false; - if (!context.mounted) return false; - - if (!selection.isCollapsed) return false; - - await editorState.insertTextAtPosition( - character, - position: selection.start, - ); - - emojiMenuService?.show(character); - return true; - } - - return false; -} diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart deleted file mode 100644 index b1b1e7cdbb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra/size.dart'; - -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; - -import 'emoji_menu.dart'; - -class EmojiHandler extends StatefulWidget { - const EmojiHandler({ - super.key, - required this.editorState, - required this.menuService, - required this.onDismiss, - required this.onSelectionUpdate, - required this.onEmojiSelect, - this.cancelBySpaceHandler, - this.initialSearchText = '', - }); - - final EditorState editorState; - final EmojiMenuService menuService; - final VoidCallback onDismiss; - final VoidCallback onSelectionUpdate; - final SelectEmojiItemHandler onEmojiSelect; - final String initialSearchText; - final bool Function()? cancelBySpaceHandler; - - @override - State createState() => _EmojiHandlerState(); -} - -class _EmojiHandlerState extends State { - final focusNode = FocusNode(debugLabel: 'emoji_menu_handler'); - final scrollController = ScrollController(); - late EmojiData emojiData; - final List searchedEmojis = []; - bool loaded = false; - int invalidCounter = 0; - late int startOffset; - late String _search = widget.initialSearchText; - double emojiHeight = 36.0; - final configuration = EmojiPickerConfiguration( - defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, - ); - - int get startCharAmount => widget.initialSearchText.length; - - set search(String search) { - _search = search; - _doSearch(); - } - - final ValueNotifier selectedIndexNotifier = ValueNotifier(0); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => focusNode.requestFocus(), - ); - - startOffset = - (widget.editorState.selection?.endIndex ?? 0) - startCharAmount; - - if (kCachedEmojiData != null) { - loadEmojis(kCachedEmojiData!); - } else { - EmojiData.builtIn().then( - (value) { - kCachedEmojiData = value; - loadEmojis(value); - }, - ); - } - } - - @override - void dispose() { - focusNode.dispose(); - selectedIndexNotifier.dispose(); - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final noEmojis = searchedEmojis.isEmpty; - return Focus( - focusNode: focusNode, - onKeyEvent: onKeyEvent, - child: Container( - constraints: const BoxConstraints(maxHeight: 392, maxWidth: 360), - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withAlpha(25), - ), - ], - ), - child: noEmojis ? buildLoading() : buildEmojis(), - ), - ); - } - - Widget buildLoading() { - return SizedBox( - width: 400, - height: 40, - child: Center( - child: SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ), - ), - ); - } - - Widget buildEmojis() { - return SizedBox( - height: - (searchedEmojis.length / configuration.perLine).ceil() * emojiHeight, - child: GridView.builder( - controller: scrollController, - itemCount: searchedEmojis.length, - padding: const EdgeInsets.symmetric(horizontal: 16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: configuration.perLine, - ), - itemBuilder: (context, index) { - final currentEmoji = searchedEmojis[index]; - final emojiId = currentEmoji.id; - final emoji = emojiData.getEmojiById( - emojiId, - skinTone: configuration.defaultSkinTone, - ); - return ValueListenableBuilder( - valueListenable: selectedIndexNotifier, - builder: (context, value, child) { - final isSelected = value == index; - return SizedBox.square( - dimension: emojiHeight, - child: FlowyButton( - isSelected: isSelected, - margin: EdgeInsets.zero, - radius: Corners.s8Border, - text: ManualTooltip( - key: ValueKey('$emojiId-$isSelected'), - message: currentEmoji.name, - showAutomaticlly: isSelected, - preferBelow: false, - child: FlowyText.emoji( - emoji, - fontSize: configuration.emojiSize, - ), - ), - onTap: () => onSelect(index), - ), - ); - }, - ); - }, - ), - ); - } - - void changeSelectedIndex(int index) => selectedIndexNotifier.value = index; - - void loadEmojis(EmojiData data) { - emojiData = data; - searchedEmojis.clear(); - searchedEmojis.addAll(emojiData.emojis.values); - if (mounted) { - setState(() { - loaded = true; - }); - } - WidgetsBinding.instance.addPostFrameCallback((_) { - _doSearch(); - }); - } - - void _doSearch() { - if (!loaded || !mounted) return; - final enableEmptySearch = widget.initialSearchText.isEmpty; - if ((_search.startsWith(' ') || _search.isEmpty) && !enableEmptySearch) { - widget.onDismiss.call(); - return; - } - final searchEmojiData = emojiData.filterByKeyword(_search); - setState(() { - searchedEmojis.clear(); - searchedEmojis.addAll(searchEmojiData.emojis.values); - changeSelectedIndex(0); - _scrollToItem(); - }); - if (searchedEmojis.isEmpty) { - widget.onDismiss.call(); - } - } - - KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - const moveKeys = [ - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - ]; - - if (event.logicalKey == LogicalKeyboardKey.enter) { - onSelect(selectedIndexNotifier.value); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - // Workaround to bring focus back to editor - widget.editorState - .updateSelectionWithReason(widget.editorState.selection); - widget.onDismiss.call(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - if (_search.isEmpty) { - if (widget.initialSearchText.isEmpty) { - widget.onDismiss.call(); - return KeyEventResult.handled; - } - if (_canDeleteLastCharacter()) { - widget.editorState.deleteBackward(); - } else { - // Workaround for editor regaining focus - widget.editorState.apply( - widget.editorState.transaction - ..afterSelection = widget.editorState.selection, - ); - } - widget.onDismiss.call(); - } else { - widget.onSelectionUpdate(); - widget.editorState.deleteBackward(); - _deleteCharacterAtSelection(); - } - - return KeyEventResult.handled; - } else if (event.character != null && - !moveKeys.contains(event.logicalKey)) { - /// Prevents dismissal of context menu by notifying the parent - /// that the selection change occurred from the handler. - widget.onSelectionUpdate(); - - if (event.logicalKey == LogicalKeyboardKey.space) { - final cancelBySpaceHandler = widget.cancelBySpaceHandler; - if (cancelBySpaceHandler != null && cancelBySpaceHandler()) { - return KeyEventResult.handled; - } - } - - // Interpolation to avoid having a getter for private variable - _insertCharacter(event.character!); - return KeyEventResult.handled; - } else if (moveKeys.contains(event.logicalKey)) { - _moveSelection(event.logicalKey); - return KeyEventResult.handled; - } - - return KeyEventResult.handled; - } - - void onSelect(int index) { - widget.onEmojiSelect.call( - context, - (startOffset - startCharAmount, startOffset + _search.length), - emojiData.getEmojiById(searchedEmojis[index].id), - ); - widget.onDismiss.call(); - } - - void _insertCharacter(String character) { - widget.editorState.insertTextAtCurrentSelection(character); - - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final delta = widget.editorState.getNodeAtPath(selection.end.path)?.delta; - if (delta == null) { - return; - } - - search = widget.editorState - .getTextInSelection( - selection.copyWith( - start: selection.start.copyWith(offset: startOffset), - end: selection.start - .copyWith(offset: startOffset + _search.length + 1), - ), - ) - .join(); - } - - void _moveSelection(LogicalKeyboardKey key) { - final index = selectedIndexNotifier.value, - perLine = configuration.perLine, - remainder = index % perLine, - length = searchedEmojis.length, - currentLine = index ~/ perLine, - maxLine = (length / perLine).ceil(); - - final heightBefore = currentLine * emojiHeight; - if (key == LogicalKeyboardKey.arrowUp) { - if (currentLine == 0) { - final exceptLine = max(0, maxLine - 1); - changeSelectedIndex(min(exceptLine * perLine + remainder, length - 1)); - } else if (currentLine > 0) { - changeSelectedIndex(index - perLine); - } - } else if (key == LogicalKeyboardKey.arrowDown) { - if (currentLine == maxLine - 1) { - changeSelectedIndex(remainder); - } else if (currentLine < maxLine - 1) { - changeSelectedIndex(min(index + perLine, length - 1)); - } - } else if (key == LogicalKeyboardKey.arrowLeft) { - if (index == 0) { - changeSelectedIndex(length - 1); - } else if (index > 0) { - changeSelectedIndex(index - 1); - } - } else if (key == LogicalKeyboardKey.arrowRight) { - if (index == length - 1) { - changeSelectedIndex(0); - } else if (index < length - 1) { - changeSelectedIndex(index + 1); - } - } - final heightAfter = - (selectedIndexNotifier.value ~/ configuration.perLine) * emojiHeight; - - if (mounted && (heightAfter != heightBefore)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToItem(); - }); - } - } - - void _scrollToItem() { - final noEmojis = searchedEmojis.isEmpty; - if (noEmojis || !mounted) return; - final currentItem = selectedIndexNotifier.value; - final exceptHeight = (currentItem ~/ configuration.perLine) * emojiHeight; - final maxExtent = scrollController.position.maxScrollExtent; - final jumpTo = (exceptHeight - maxExtent > 10 * emojiHeight) - ? exceptHeight - : min(exceptHeight, maxExtent); - scrollController.animateTo( - jumpTo, - duration: Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - - void _deleteCharacterAtSelection() { - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = widget.editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - search = delta.toPlainText().substring( - startOffset, - startOffset + _search.length - 1, - ); - } - - bool _canDeleteLastCharacter() { - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return false; - } - - final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta; - if (delta == null) { - return false; - } - - return delta.isNotEmpty; - } -} - -typedef SelectEmojiItemHandler = void Function( - BuildContext context, - (int start, int end) replacement, - String emoji, -); diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart deleted file mode 100644 index 29f130d77d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'emoji_actions_command.dart'; -import 'emoji_handler.dart'; - -abstract class EmojiMenuService { - void show(String character); - - void dismiss(); -} - -class EmojiMenu extends EmojiMenuService { - EmojiMenu({ - required this.overlay, - required this.editorState, - this.cancelBySpaceHandler, - this.menuHeight = 400, - this.menuWidth = 300, - }); - - final EditorState editorState; - final double menuHeight; - final double menuWidth; - final OverlayState overlay; - final bool Function()? cancelBySpaceHandler; - - Offset _offset = Offset.zero; - Alignment _alignment = Alignment.topLeft; - OverlayEntry? _menuEntry; - bool selectionChangedByMenu = false; - String initialCharacter = ''; - - @override - void dismiss() { - if (_menuEntry != null) { - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); - keepEditorFocusNotifier.decrease(); - } - - _menuEntry?.remove(); - _menuEntry = null; - - // workaround: SelectionService has been released after hot reload. - final isSelectionDisposed = - editorState.service.selectionServiceKey.currentState == null; - if (!isSelectionDisposed) { - final selectionService = editorState.service.selectionService; - selectionService.currentSelection.removeListener(_onSelectionChange); - } - emojiMenuService = null; - } - - void _onSelectionUpdate() => selectionChangedByMenu = true; - - @override - void show(String character) { - initialCharacter = character; - WidgetsBinding.instance.addPostFrameCallback((_) => _show()); - } - - void _show() { - final selectionService = editorState.service.selectionService; - final selectionRects = selectionService.selectionRects; - if (selectionRects.isEmpty) { - return; - } - - final Size editorSize = editorState.renderBox!.size; - - calculateSelectionMenuOffset(selectionRects.first); - - final (left, top, right, bottom) = _getPosition(); - - _menuEntry = OverlayEntry( - builder: (context) => SizedBox( - height: editorSize.height, - width: editorSize.width, - - // GestureDetector handles clicks outside of the context menu, - // to dismiss the context menu. - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - Positioned( - top: top, - bottom: bottom, - left: left, - right: right, - child: EmojiHandler( - editorState: editorState, - menuService: this, - onDismiss: dismiss, - onSelectionUpdate: _onSelectionUpdate, - cancelBySpaceHandler: cancelBySpaceHandler, - initialSearchText: initialCharacter, - onEmojiSelect: ( - BuildContext context, - (int, int) replacement, - String emoji, - ) async { - final selection = editorState.selection; - - if (selection == null) return; - final node = - editorState.document.nodeAtPath(selection.end.path); - if (node == null) return; - final transaction = editorState.transaction - ..deleteText( - node, - replacement.$1, - replacement.$2 - replacement.$1, - ) - ..insertText( - node, - replacement.$1, - emoji, - ); - await editorState.apply(transaction); - }, - ), - ), - ], - ), - ), - ), - ); - - overlay.insert(_menuEntry!); - - keepEditorFocusNotifier.increase(); - editorState.service.keyboardService?.disable(showCursor: true); - editorState.service.scrollService?.disable(); - selectionService.currentSelection.addListener(_onSelectionChange); - } - - void _onSelectionChange() { - // workaround: SelectionService has been released after hot reload. - final isSelectionDisposed = - editorState.service.selectionServiceKey.currentState == null; - if (!isSelectionDisposed) { - final selectionService = editorState.service.selectionService; - if (selectionService.currentSelection.value == null) { - return; - } - } - - if (!selectionChangedByMenu) { - return dismiss(); - } - - selectionChangedByMenu = false; - } - - (double? left, double? top, double? right, double? bottom) _getPosition() { - double? left, top, right, bottom; - switch (_alignment) { - case Alignment.topLeft: - left = _offset.dx; - top = _offset.dy; - break; - case Alignment.bottomLeft: - left = _offset.dx; - bottom = _offset.dy; - break; - case Alignment.topRight: - right = _offset.dx; - top = _offset.dy; - break; - case Alignment.bottomRight: - right = _offset.dx; - bottom = _offset.dy; - break; - } - - return (left, top, right, bottom); - } - - void calculateSelectionMenuOffset(Rect rect) { - // Workaround: We can customize the padding through the [EditorStyle], - // but the coordinates of overlay are not properly converted currently. - // Just subtract the padding here as a result. - const menuOffset = Offset(0, 10); - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = editorState.renderBox!.size.height; - final editorWidth = editorState.renderBox!.size.width; - - // show below default - _alignment = Alignment.topLeft; - final bottomRight = rect.bottomRight; - final topRight = rect.topRight; - var offset = bottomRight + menuOffset; - _offset = Offset( - offset.dx, - offset.dy, - ); - - // show above - if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { - offset = topRight - menuOffset; - _alignment = Alignment.bottomLeft; - - _offset = Offset( - offset.dx, - editorHeight + editorOffset.dy - offset.dy, - ); - } - - // show on right - if (_offset.dx + menuWidth < editorOffset.dx + editorWidth) { - _offset = Offset( - _offset.dx, - _offset.dy, - ); - } else if (offset.dx - editorOffset.dx > menuWidth) { - // show on left - _alignment = _alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - _offset = Offset( - editorWidth - _offset.dx + editorOffset.dx, - _offset.dy, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart index 6dbd38affb..d29a1f86bf 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart @@ -24,7 +24,7 @@ class InlineChildPageService extends InlineActionsDelegate { results.add( InlineActionsMenuItem( label: LocaleKeys.inlineActions_createPage.tr(args: [search]), - iconBuilder: (_) => const FlowySvg(FlowySvgs.add_s), + icon: (_) => const FlowySvg(FlowySvgs.add_s), onSelected: (context, editorState, service, replacement) => _onSelected(context, editorState, service, replacement, search), ), @@ -71,11 +71,12 @@ class InlineChildPageService extends InlineActionsDelegate { replacement.$1, replacement.$2, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.childPage, - pageId: view.id, - blockId: null, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.childPage.name, + MentionBlockKeys.pageId: view.id, + }, + }, ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index 747c8667f8..c7076bd255 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -122,12 +122,12 @@ class DateReferenceService extends InlineActionsDelegate { start, end, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - includeTime: false, - reminderId: null, - reminderOption: null, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + }, + }, ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 9853d6757c..27a632e8ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -221,11 +221,12 @@ class InlinePageReferenceService extends InlineActionsDelegate { replace.$1, replace.$2, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + }, + }, ); await editorState.apply(transaction); @@ -234,19 +235,12 @@ class InlinePageReferenceService extends InlineActionsDelegate { InlineActionsMenuItem _fromView(ViewPB view) => InlineActionsMenuItem( keywords: [view.nameOrDefault.toLowerCase()], label: view.nameOrDefault, - iconBuilder: (onSelected) { - final child = view.icon.value.isNotEmpty - ? RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 16.0, - lineHeight: 18.0 / 16.0, - ) - : view.defaultIcon(size: const Size(16, 16)); - return SizedBox( - width: 16, - child: child, - ); - }, + icon: (onSelected) => view.icon.value.isNotEmpty + ? RawEmojiIconWidget( + emoji: view.icon.toEmojiIconData(), + emojiSize: 14, + ) + : view.defaultIcon(), onSelected: (context, editorState, menu, replace) => insertPage ? _onInsertPageRef(view, context, editorState, replace) : _onInsertLinkRef(view, context, editorState, menu, replace), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index 471f1c9211..1f479fa7c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -148,12 +148,14 @@ class ReminderReferenceService extends InlineActionsDelegate { start, end, MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - reminderId: reminder.id, - reminderOption: ReminderOption.atTimeOfEvent.name, - includeTime: false, - ), + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + MentionBlockKeys.reminderId: reminder.id, + MentionBlockKeys.reminderOption: ReminderOption.atTimeOfEvent.name, + }, + }, ); await editorState.apply(transaction); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart index 1fe2703870..8da9647084 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart @@ -12,13 +12,13 @@ typedef SelectItemHandler = void Function( class InlineActionsMenuItem { InlineActionsMenuItem({ required this.label, - this.iconBuilder, + this.icon, this.keywords, this.onSelected, }); final String label; - final Widget Function(bool onSelected)? iconBuilder; + final Widget Function(bool onSelected)? icon; final List? keywords; final SelectItemHandler? onSelected; } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart index 123cfc1177..f2d22138f7 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart @@ -92,8 +92,8 @@ class InlineActionsWidget extends StatefulWidget { class _InlineActionsWidgetState extends State { @override Widget build(BuildContext context) { - final iconBuilder = widget.item.iconBuilder; - final hasIcon = iconBuilder != null; + final icon = widget.item.icon; + final hasIcon = icon != null; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( @@ -104,7 +104,7 @@ class _InlineActionsWidgetState extends State { text: Row( children: [ if (hasIcon) ...[ - iconBuilder.call(widget.isSelected), + icon.call(widget.isSelected), SizedBox(width: 12), ], Flexible( diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index 9d6adee7df..f788d99eb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -70,7 +70,7 @@ class ExportTab extends StatelessWidget { const VSpace(10), _ExportButton( title: LocaleKeys.shareAction_csv.tr(), - svg: FlowySvgs.database_layout_s, + svg: FlowySvgs.database_layout_m, onTap: () => _exportCSV(context), ), if (kDebugMode) ...[ @@ -174,10 +174,11 @@ class ExportTab extends StatelessWidget { ClipboardServiceData(plainText: markdown), ); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), ); }, - (error) => showToastNotification(message: error.msg), + (error) => showToastNotification(context, message: error.msg), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 244ded0bf6..1f754a4372 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -85,9 +85,11 @@ class PublishTab extends StatelessWidget { if (state.publishResult != null) { state.publishResult!.fold( (value) => showToastNotification( + context, message: LocaleKeys.publish_publishSuccessfully.tr(), ), (error) => showToastNotification( + context, message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', type: ToastificationType.error, ), @@ -95,9 +97,11 @@ class PublishTab extends StatelessWidget { } else if (state.unpublishResult != null) { state.unpublishResult!.fold( (value) => showToastNotification( + context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ), (error) => showToastNotification( + context, message: LocaleKeys.publish_unpublishFailed.tr(), description: error.msg, type: ToastificationType.error, @@ -106,12 +110,14 @@ class PublishTab extends StatelessWidget { } else if (state.updatePathNameResult != null) { state.updatePathNameResult!.fold( (value) => showToastNotification( + context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ), (error) { Log.error('update path name failed: $error'); showToastNotification( + context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: error.code.publishErrorMessage, @@ -176,7 +182,8 @@ class _PublishedWidgetState extends State<_PublishedWidget> { ); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copy.tr(), ); }, onSubmitted: (pathName) { @@ -285,6 +292,7 @@ class _PublishWidgetState extends State<_PublishWidget> { // check if any database is selected if (_selectedViews.isEmpty) { showToastNotification( + context, message: LocaleKeys.publish_noDatabaseSelected.tr(), ); return; @@ -603,6 +611,7 @@ class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { // unable to deselect the primary database if (isPrimaryDatabase) { showToastNotification( + context, message: LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart index e683518526..a852fa5e38 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart @@ -193,7 +193,7 @@ class ShareBloc extends Bloc { Future _updatePublishStatus(Emitter emit) async { final publishInfo = await ViewBackendService.getPublishInfo(view); final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.workspaceAuthType == AuthTypePB.Server, + (v) => v.authenticator == AuthenticatorPB.AppFlowyCloud, (p) => false, ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart index 9020441b4e..59ee55b980 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -70,6 +70,7 @@ class ShareButton extends StatelessWidget { case ShareType.html: case ShareType.csv: showToastNotification( + context, message: LocaleKeys.settings_files_exportFileSuccess.tr(), ); break; @@ -80,6 +81,7 @@ class ShareButton extends StatelessWidget { void _handleExportError(BuildContext context, FlowyError error) { showToastNotification( + context, message: '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}', ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart index 190fe9ddd8..6980edce46 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart @@ -117,7 +117,8 @@ class _ShareTabContent extends StatelessWidget { ); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copy.tr(), ); } } diff --git a/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart b/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart deleted file mode 100644 index 2632c22d49..0000000000 --- a/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; - -extension UserProfilePBExtension on UserProfilePB { - String? get authToken { - try { - final map = jsonDecode(token) as Map; - return map['access_token'] as String?; - } catch (e) { - Log.error('Failed to decode auth token: $e'); - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart index 5942271206..da9f679f56 100644 --- a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart +++ b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart @@ -45,6 +45,7 @@ class _MobileSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( + context, message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); @@ -100,6 +101,7 @@ class _DesktopSyncErrorPage extends StatelessWidget { onTapUp: () { getIt().setPlainText(error.toString()); showToastNotification( + context, message: LocaleKeys.message_copy_success.tr(), bottomPadding: 0, ); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index c303160ffe..2974156a2a 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -13,7 +13,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/util/default_extensions.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -294,8 +294,8 @@ class _IconUploaderState extends State { (userProfile) => userProfile, (l) => null, ); - final isLocalMode = (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == - AuthTypePB.Local; + final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == + AuthenticatorPB.Local; if (isLocalMode) { result = await pickedImages.first.saveToLocal(); } else { diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 912f96bd05..4be6fdbe11 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -27,7 +27,6 @@ Future customDocumentToMarkdown( Document document, { String path = '', AsyncValueSetter? onArchive, - String lineBreak = '', }) async { final List> fileFutures = []; @@ -42,7 +41,6 @@ Future customDocumentToMarkdown( try { markdown = documentToMarkdown( document, - lineBreak: lineBreak, customParsers: [ const MathEquationNodeParser(), const CalloutNodeParser(), diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 5a8c0fa651..621ba988cf 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -102,7 +102,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { case AuthenticatorType.local: getIt.registerFactory( () => BackendAuthService( - AuthTypePB.Local, + AuthenticatorPB.Local, ), ); break; diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 7a282b3856..7fa18fc54d 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; import 'package:appflowy/util/expand_views.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; @@ -121,7 +119,7 @@ class FlowyRunner { // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. MemoryLeakDetectorTask(), - DebugTask(), + const DebugTask(), const FeatureFlagTask(), // localization @@ -187,10 +185,6 @@ Future initGetIt( ); getIt.registerSingleton(PluginSandbox()); getIt.registerSingleton(ViewExpanderRegistry()); - getIt.registerSingleton(LinkHoverTriggers()); - getIt.registerSingleton( - FloatingToolbarController(), - ); await DependencyResolver.resolve(getIt, mode); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 48e76cecbc..a398db3061 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -7,21 +7,19 @@ import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/notification/notification_service.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -66,6 +64,7 @@ class InitAppWidgetTask extends LaunchTask { child: widget, ); + Bloc.observer = ApplicationBlocObserver(); runApp( EasyLocalization( supportedLocales: const [ @@ -101,7 +100,6 @@ class InitAppWidgetTask extends LaunchTask { Locale('zh', 'TW'), Locale('fa'), Locale('hin'), - Locale('mr', 'IN'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), @@ -140,8 +138,6 @@ class _ApplicationWidgetState extends State { final _commandPaletteNotifier = ValueNotifier(false); - final themeBuilder = AppFlowyDefaultTheme(); - @override void initState() { super.initState(); @@ -230,6 +226,22 @@ class _ApplicationWidgetState extends State { } }, child: MaterialApp.router( + builder: (context, child) => MediaQuery( + // use the 1.0 as the textScaleFactor to avoid the text size + // affected by the system setting. + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(state.textScaleFactor), + ), + child: overlayManagerBuilder( + context, + !UniversalPlatform.isMobile && FeatureFlag.search.isOn + ? CommandPalette( + notifier: _commandPaletteNotifier, + child: child, + ) + : child, + ), + ), debugShowCheckedModeBanner: false, theme: state.lightTheme, darkTheme: state.darkTheme, @@ -238,35 +250,6 @@ class _ApplicationWidgetState extends State { supportedLocales: context.supportedLocales, locale: state.locale, routerConfig: routerConfig, - builder: (context, child) { - final brightness = Theme.of(context).brightness; - final fontFamily = - state.font.orDefault(defaultFontFamily); - - return AppFlowyTheme( - data: brightness == Brightness.light - ? themeBuilder.light(fontFamily: fontFamily) - : themeBuilder.dark(fontFamily: fontFamily), - child: MediaQuery( - // use the 1.0 as the textScaleFactor to avoid the text size - // affected by the system setting. - data: MediaQuery.of(context).copyWith( - textScaler: - TextScaler.linear(state.textScaleFactor), - ), - child: overlayManagerBuilder( - context, - !UniversalPlatform.isMobile && - FeatureFlag.search.isOn - ? CommandPalette( - notifier: _commandPaletteNotifier, - child: child, - ) - : child, - ), - ), - ); - }, ), ), ), @@ -300,6 +283,14 @@ class AppGlobals { static BuildContext get context => rootNavKey.currentContext!; } +class ApplicationBlocObserver extends BlocObserver { + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + Log.debug(error); + super.onError(bloc, error, stackTrace); + } +} + Future appTheme(String themeName) async { if (themeName.isEmpty) { return AppTheme.fallback; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart index 5636ed70cb..7d41f2dceb 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart @@ -12,10 +12,6 @@ class WindowSizeManager { static const double maxWindowHeight = 8192.0; static const double maxWindowWidth = 8192.0; - // Default windows size - static const double defaultWindowHeight = 960.0; - static const double defaultWindowWidth = 1280.0; - static const double maxScaleFactor = 2.0; static const double minScaleFactor = 0.5; @@ -40,8 +36,8 @@ class WindowSizeManager { Future getSize() async { final defaultWindowSize = jsonEncode( { - WindowSizeManager.height: defaultWindowHeight, - WindowSizeManager.width: defaultWindowWidth, + WindowSizeManager.height: minWindowHeight, + WindowSizeManager.width: minWindowWidth, }, ); final windowSize = await getIt().get(KVKeys.windowSize); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 362b27a85a..2c22b8a01e 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -83,13 +83,6 @@ class AppFlowyCloudDeepLink { void unsubscribeDeepLinkLoadingState(VoidCallback listener) => _stateNotifier?.removeListener(listener); - Future passGotrueTokenResponse( - GotrueTokenResponsePB gotrueTokenResponse, - ) async { - final uri = _buildDeepLinkUri(gotrueTokenResponse); - await _handleUri(uri); - } - Future _handleUri( Uri? uri, ) async { @@ -112,7 +105,7 @@ class AppFlowyCloudDeepLink { (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, + authenticator: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: uri.toString(), AuthServiceMapKeys.deviceId: deviceId, @@ -136,6 +129,7 @@ class AppFlowyCloudDeepLink { final context = AppGlobals.rootNavKey.currentState?.context; if (context != null) { showToastNotification( + context, message: err.msg, ); } @@ -179,57 +173,6 @@ class AppFlowyCloudDeepLink { bool _isPaymentSuccessUri(Uri uri) { return uri.host == 'payment-success'; } - - Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) { - final params = {}; - - if (gotrueTokenResponse.hasAccessToken() && - gotrueTokenResponse.accessToken.isNotEmpty) { - params['access_token'] = gotrueTokenResponse.accessToken; - } - - if (gotrueTokenResponse.hasExpiresAt()) { - params['expires_at'] = gotrueTokenResponse.expiresAt.toString(); - } - - if (gotrueTokenResponse.hasExpiresIn()) { - params['expires_in'] = gotrueTokenResponse.expiresIn.toString(); - } - - if (gotrueTokenResponse.hasProviderRefreshToken() && - gotrueTokenResponse.providerRefreshToken.isNotEmpty) { - params['provider_refresh_token'] = - gotrueTokenResponse.providerRefreshToken; - } - - if (gotrueTokenResponse.hasProviderAccessToken() && - gotrueTokenResponse.providerAccessToken.isNotEmpty) { - params['provider_token'] = gotrueTokenResponse.providerAccessToken; - } - - if (gotrueTokenResponse.hasRefreshToken() && - gotrueTokenResponse.refreshToken.isNotEmpty) { - params['refresh_token'] = gotrueTokenResponse.refreshToken; - } - - if (gotrueTokenResponse.hasTokenType() && - gotrueTokenResponse.tokenType.isNotEmpty) { - params['token_type'] = gotrueTokenResponse.tokenType; - } - - if (params.isEmpty) { - return null; - } - - final fragment = params.entries - .map( - (e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}', - ) - .join('&'); - - return Uri.parse('appflowy-flutter://login-callback#$fragment'); - } } class InitAppFlowyCloudTask extends LaunchTask { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart index 9a34e84f70..082e25e250 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart @@ -1,45 +1,18 @@ -import 'package:appflowy/startup/startup.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:talker/talker.dart'; -import 'package:talker_bloc_logger/talker_bloc_logger.dart'; import 'package:universal_platform/universal_platform.dart'; -class DebugTask extends LaunchTask { - DebugTask(); +import '../startup.dart'; - final Talker talker = Talker(); +class DebugTask extends LaunchTask { + const DebugTask(); @override Future initialize(LaunchContext context) async { - // hide the keyboard on mobile + // the hotkey manager is not supported on mobile if (UniversalPlatform.isMobile && kDebugMode) { await SystemChannels.textInput.invokeMethod('TextInput.hide'); } - - // log the bloc events - if (kDebugMode) { - Bloc.observer = TalkerBlocObserver( - talker: talker, - settings: TalkerBlocLoggerSettings( - // Disabled by default to prevent mixing with AppFlowy logs - // Enable to observe all bloc events - enabled: false, - printEventFullData: false, - printStateFullData: false, - printChanges: true, - printClosings: true, - printCreations: true, - transitionFilter: (_, transition) { - // By default, observe all transitions - // You can add your own filter here if needed - // when you want to observer a specific bloc - return true; - }, - ), - ); - } } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart index 2c90afbdda..81464f59d7 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart @@ -29,9 +29,6 @@ class ApplicationInfo { // If the latest version is greater than the current version, it means there is an update available static bool get isUpdateAvailable { try { - if (latestVersion.isEmpty) { - return false; - } return Version.parse(latestVersion) > Version.parse(applicationVersion); } catch (e) { return false; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index e64e0f98de..b326276c56 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -51,6 +51,7 @@ GoRouter generateRouter(Widget child) { // Routes in both desktop and mobile _signInScreenRoute(), _skipLogInScreenRoute(), + _encryptSecretScreenRoute(), _workspaceErrorScreenRoute(), // Desktop only if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(), @@ -119,6 +120,18 @@ GoRouter generateRouter(Widget child) { ); }, ), + GoRoute( + path: SignUpScreen.routeName, + pageBuilder: (context, state) { + return CustomTransitionPage( + child: SignUpScreen( + router: getIt(), + ), + transitionsBuilder: _buildFadeTransition, + transitionDuration: _slowDuration, + ); + }, + ), ], ); } @@ -458,6 +471,23 @@ GoRoute _workspaceErrorScreenRoute() { ); } +GoRoute _encryptSecretScreenRoute() { + return GoRoute( + path: EncryptSecretScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + return CustomTransitionPage( + child: EncryptSecretScreen( + user: args[EncryptSecretScreen.argUser], + key: args[EncryptSecretScreen.argKey], + ), + transitionsBuilder: _buildFadeTransition, + transitionDuration: _slowDuration, + ); + }, + ); +} + GoRoute _skipLogInScreenRoute() { return GoRoute( path: SkipLogInScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index c406dd161a..a0f5b0bafe 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -5,6 +5,7 @@ import 'package:appflowy/env/backend_env.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -28,6 +29,7 @@ class InitRustSDKTask extends LaunchTask { final dir = customApplicationPath ?? applicationPath; final deviceId = await getDeviceId(); + debugPrint('application path: ${applicationPath.path}'); // Pass the environment variables to the Rust SDK final env = _makeAppFlowyConfiguration( root.path, @@ -73,10 +75,10 @@ Future appFlowyApplicationDataDirectory() async { case IntegrationMode.develop: final Directory documentsDir = await getApplicationSupportDirectory() .then((directory) => directory.create()); - return Directory(path.join(documentsDir.path, 'data_dev')); + return Directory(path.join(documentsDir.path, 'data_dev')).create(); case IntegrationMode.release: final Directory documentsDir = await getApplicationSupportDirectory(); - return Directory(path.join(documentsDir.path, 'data')); + return Directory(path.join(documentsDir.path, 'data')).create(); case IntegrationMode.unitTest: case IntegrationMode.integrationTest: return Directory(path.join(Directory.current.path, '.sandbox')); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 4f4cece9bb..149bddc951 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -18,7 +18,7 @@ class AppFlowyCloudAuthService implements AuthService { AppFlowyCloudAuthService(); final BackendAuthService _backendAuthService = BackendAuthService( - AuthTypePB.Server, + AuthenticatorPB.AppFlowyCloud, ); @override @@ -32,17 +32,12 @@ class AppFlowyCloudAuthService implements AuthService { } @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, }) async { - return _backendAuthService.signInWithEmailPassword( - email: email, - password: password, - params: params, - ); + throw UnimplementedError(); } @override @@ -111,17 +106,6 @@ class AppFlowyCloudAuthService implements AuthService { ); } - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - return _backendAuthService.signInWithPasscode( - email: email, - passcode: passcode, - ); - } - @override Future> getUser() async { return UserBackendService.getCurrentUserProfile(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index 8be71dc648..5f8ea7cac6 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -20,7 +20,7 @@ class AppFlowyCloudMockAuthService implements AuthService { final String userEmail; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthTypePB.Server); + BackendAuthService(AuthenticatorPB.AppFlowyCloud); @override Future> signUp({ @@ -33,8 +33,7 @@ class AppFlowyCloudMockAuthService implements AuthService { } @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -48,7 +47,7 @@ class AppFlowyCloudMockAuthService implements AuthService { Map params = const {}, }) async { final payload = SignInUrlPayloadPB.create() - ..authenticator = AuthTypePB.Server + ..authenticator = AuthenticatorPB.AppFlowyCloud // don't use nanoid here, the gotrue server will transform the email ..email = userEmail; @@ -58,7 +57,7 @@ class AppFlowyCloudMockAuthService implements AuthService { return getSignInURLResult.fold( (urlPB) async { final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, + authenticator: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: urlPB.signInUrl, AuthServiceMapKeys.deviceId: deviceId, @@ -107,12 +106,4 @@ class AppFlowyCloudMockAuthService implements AuthService { Future> getUser() async { return UserBackendService.getCurrentUserProfile(); } - - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - throw UnimplementedError(); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart index 9879b9a18e..90c6954afe 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; import 'package:appflowy_result/appflowy_result.dart'; class AuthServiceMapKeys { @@ -23,8 +23,7 @@ abstract class AuthService { /// /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params, @@ -76,17 +75,6 @@ abstract class AuthService { Map params, }); - /// Authenticates a user with a passcode sent to their email. - /// - /// - `email`: The email address of the user. - /// - `passcode`: The passcode of the user. - /// - /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signInWithPasscode({ - required String email, - required String passcode, - }); - /// Signs out the currently authenticated user. Future signOut(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index cab8cd170c..9147fb4fb9 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -6,9 +6,9 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; import '../../../generated/locale_keys.g.dart'; import 'device_id.dart'; @@ -16,11 +16,10 @@ import 'device_id.dart'; class BackendAuthService implements AuthService { BackendAuthService(this.authType); - final AuthTypePB authType; + final AuthenticatorPB authType; @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -30,7 +29,8 @@ class BackendAuthService implements AuthService { ..password = password ..authType = authType ..deviceId = await getDeviceId(); - return UserEventSignInWithEmailPassword(request).send(); + final response = UserEventSignInWithEmailPassword(request).send(); + return response.then((value) => value); } @override @@ -65,14 +65,15 @@ class BackendAuthService implements AuthService { Map params = const {}, }) async { const password = "Guest!@123456"; - final userEmail = "anon@appflowy.io"; + final uid = uuid(); + final userEmail = "$uid@appflowy.io"; final request = SignUpPayloadPB.create() ..name = LocaleKeys.defaultUsername.tr() ..email = userEmail ..password = password // When sign up as guest, the auth type is always local. - ..authType = AuthTypePB.Local + ..authType = AuthenticatorPB.Local ..deviceId = await getDeviceId(); final response = await UserEventSignUp(request).send().then( (value) => value, @@ -83,7 +84,7 @@ class BackendAuthService implements AuthService { @override Future> signUpWithOAuth({ required String platform, - AuthTypePB authType = AuthTypePB.Local, + AuthenticatorPB authType = AuthenticatorPB.Local, Map params = const {}, }) async { return FlowyResult.failure( @@ -106,12 +107,4 @@ class BackendAuthService implements AuthService { // No need to pass the redirect URL. return UserBackendService.signInWithMagicLink(email, ''); } - - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - return UserBackendService.signInWithPasscode(email, passcode); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart new file mode 100644 index 0000000000..19b8101ae8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'auth/auth_service.dart'; + +part 'encrypt_secret_bloc.freezed.dart'; + +class EncryptSecretBloc extends Bloc { + EncryptSecretBloc({required this.user}) + : super(EncryptSecretState.initial()) { + _dispatch(); + } + + final UserProfilePB user; + + void _dispatch() { + on((event, emit) async { + await event.when( + setEncryptSecret: (secret) async { + if (isLoading()) { + return; + } + + final payload = UserSecretPB.create() + ..encryptionSecret = secret + ..encryptionSign = user.encryptionSign + ..encryptionType = user.encryptionType + ..userId = user.id; + final result = await UserEventSetEncryptionSecret(payload).send(); + if (!isClosed) { + add(EncryptSecretEvent.didFinishCheck(result)); + } + emit( + state.copyWith( + loadingState: const LoadingState.loading(), + successOrFail: null, + ), + ); + }, + cancelInputSecret: () async { + await getIt().signOut(); + emit( + state.copyWith( + successOrFail: null, + isSignOut: true, + ), + ); + }, + didFinishCheck: (result) { + result.fold( + (unit) { + emit( + state.copyWith( + loadingState: const LoadingState.loading(), + successOrFail: result, + ), + ); + }, + (err) { + emit( + state.copyWith( + loadingState: LoadingState.finish(FlowyResult.failure(err)), + successOrFail: result, + ), + ); + }, + ); + }, + ); + }); + } + + bool isLoading() { + final loadingState = state.loadingState; + if (loadingState != null) { + return loadingState.when( + loading: () => true, + finish: (_) => false, + idle: () => false, + ); + } + return false; + } +} + +@freezed +class EncryptSecretEvent with _$EncryptSecretEvent { + const factory EncryptSecretEvent.setEncryptSecret(String secret) = + _SetEncryptSecret; + const factory EncryptSecretEvent.didFinishCheck( + FlowyResult result, + ) = _DidFinishCheck; + const factory EncryptSecretEvent.cancelInputSecret() = _CancelInputSecret; +} + +@freezed +class EncryptSecretState with _$EncryptSecretState { + const factory EncryptSecretState({ + required FlowyResult? successOrFail, + required bool isSignOut, + LoadingState? loadingState, + }) = _EncryptSecretState; + + factory EncryptSecretState.initial() => const EncryptSecretState( + successOrFail: null, + isSignOut: false, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart deleted file mode 100644 index 80dd5ca3c9..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/user/application/password/password_http_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'password_bloc.freezed.dart'; - -class PasswordBloc extends Bloc { - PasswordBloc(this.userProfile) : super(PasswordState.initial()) { - on( - (event, emit) async { - await event.when( - init: () async => _init(), - changePassword: (oldPassword, newPassword) async => _onChangePassword( - emit, - oldPassword: oldPassword, - newPassword: newPassword, - ), - setupPassword: (newPassword) async => _onSetupPassword( - emit, - newPassword: newPassword, - ), - forgotPassword: (email) async => _onForgotPassword( - emit, - email: email, - ), - checkHasPassword: () async => _onCheckHasPassword( - emit, - ), - cancel: () {}, - ); - }, - ); - } - - final UserProfilePB userProfile; - late final PasswordHttpService passwordHttpService; - - bool _isInitialized = false; - - Future _init() async { - if (userProfile.workspaceAuthType == AuthTypePB.Local) { - Log.debug('PasswordBloc: skip init because user is local authenticator'); - return; - } - - final baseUrl = await getAppFlowyCloudUrl(); - try { - final authToken = jsonDecode(userProfile.token)['access_token']; - passwordHttpService = PasswordHttpService( - baseUrl: baseUrl, - authToken: authToken, - ); - _isInitialized = true; - } catch (e) { - Log.error('PasswordBloc: _init: error: $e'); - } - } - - Future _onChangePassword( - Emitter emit, { - required String oldPassword, - required String newPassword, - }) async { - if (!_isInitialized) { - Log.info('changePassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('changePassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.changePassword( - currentPassword: oldPassword, - newPassword: newPassword, - ); - - emit( - state.copyWith( - isSubmitting: false, - changePasswordResult: result, - ), - ); - } - - Future _onSetupPassword( - Emitter emit, { - required String newPassword, - }) async { - if (!_isInitialized) { - Log.info('setupPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('setupPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.setupPassword( - newPassword: newPassword, - ); - - emit( - state.copyWith( - isSubmitting: false, - hasPassword: result.fold( - (success) => true, - (error) => false, - ), - setupPasswordResult: result, - ), - ); - } - - Future _onForgotPassword( - Emitter emit, { - required String email, - }) async { - if (!_isInitialized) { - Log.info('forgotPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('forgotPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.forgotPassword(email: email); - - emit( - state.copyWith( - isSubmitting: false, - forgotPasswordResult: result, - ), - ); - } - - Future _onCheckHasPassword(Emitter emit) async { - if (!_isInitialized) { - Log.info('checkHasPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('checkHasPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.checkHasPassword(); - - emit( - state.copyWith( - isSubmitting: false, - hasPassword: result.fold( - (success) => success, - (error) => false, - ), - checkHasPasswordResult: result, - ), - ); - } - - void _clearState(Emitter emit, bool isSubmitting) { - emit( - state.copyWith( - isSubmitting: isSubmitting, - changePasswordResult: null, - setupPasswordResult: null, - forgotPasswordResult: null, - checkHasPasswordResult: null, - ), - ); - } -} - -@freezed -class PasswordEvent with _$PasswordEvent { - const factory PasswordEvent.init() = Init; - - // Change password - const factory PasswordEvent.changePassword({ - required String oldPassword, - required String newPassword, - }) = ChangePassword; - - // Setup password - const factory PasswordEvent.setupPassword({ - required String newPassword, - }) = SetupPassword; - - // Forgot password - const factory PasswordEvent.forgotPassword({ - required String email, - }) = ForgotPassword; - - // Check has password - const factory PasswordEvent.checkHasPassword() = CheckHasPassword; - - // Cancel operation - const factory PasswordEvent.cancel() = Cancel; -} - -@freezed -class PasswordState with _$PasswordState { - const factory PasswordState({ - required bool isSubmitting, - required bool hasPassword, - required FlowyResult? changePasswordResult, - required FlowyResult? setupPasswordResult, - required FlowyResult? forgotPasswordResult, - required FlowyResult? checkHasPasswordResult, - }) = _PasswordState; - - factory PasswordState.initial() => const PasswordState( - isSubmitting: false, - hasPassword: false, - changePasswordResult: null, - setupPasswordResult: null, - forgotPasswordResult: null, - checkHasPasswordResult: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart deleted file mode 100644 index c56c4f595d..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:http/http.dart' as http; - -enum PasswordEndpoint { - changePassword, - forgotPassword, - setupPassword, - checkHasPassword; - - String get path { - switch (this) { - case PasswordEndpoint.changePassword: - return '/gotrue/user/change-password'; - case PasswordEndpoint.forgotPassword: - return '/gotrue/user/recover'; - case PasswordEndpoint.setupPassword: - return '/gotrue/user/change-password'; - case PasswordEndpoint.checkHasPassword: - return '/gotrue/user/auth-info'; - } - } - - String get method { - switch (this) { - case PasswordEndpoint.changePassword: - case PasswordEndpoint.setupPassword: - case PasswordEndpoint.forgotPassword: - return 'POST'; - case PasswordEndpoint.checkHasPassword: - return 'GET'; - } - } - - Uri uri(String baseUrl) => Uri.parse('$baseUrl$path'); -} - -class PasswordHttpService { - PasswordHttpService({ - required this.baseUrl, - required this.authToken, - }); - - final String baseUrl; - final String authToken; - - final http.Client client = http.Client(); - - Map get headers => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $authToken', - }; - - /// Changes the user's password - /// - /// [currentPassword] - The user's current password - /// [newPassword] - The new password to set - Future> changePassword({ - required String currentPassword, - required String newPassword, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.changePassword, - body: { - 'current_password': currentPassword, - 'password': newPassword, - }, - errorMessage: 'Failed to change password', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Sends a password reset email to the user - /// - /// [email] - The email address of the user - Future> forgotPassword({ - required String email, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.forgotPassword, - body: {'email': email}, - errorMessage: 'Failed to send password reset email', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Sets up a password for a user that doesn't have one - /// - /// [newPassword] - The new password to set - Future> setupPassword({ - required String newPassword, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.setupPassword, - body: {'password': newPassword}, - errorMessage: 'Failed to setup password', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Checks if the user has a password set - Future> checkHasPassword() async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.checkHasPassword, - errorMessage: 'Failed to check password status', - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['has_password'] ?? false), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to check password status: $e'), - ); - } - } - - /// Makes a request to the specified endpoint with the given body - Future> _makeRequest({ - required PasswordEndpoint endpoint, - Map? body, - String errorMessage = 'Request failed', - }) async { - try { - final uri = endpoint.uri(baseUrl); - http.Response response; - - if (endpoint.method == 'POST') { - response = await client.post( - uri, - headers: headers, - body: body != null ? jsonEncode(body) : null, - ); - } else if (endpoint.method == 'GET') { - response = await client.get( - uri, - headers: headers, - ); - } else { - return FlowyResult.failure( - FlowyError(msg: 'Invalid request method: ${endpoint.method}'), - ); - } - - if (response.statusCode == 200) { - if (response.body.isNotEmpty) { - return FlowyResult.success(jsonDecode(response.body)); - } - return FlowyResult.success(true); - } else { - final errorBody = - response.body.isNotEmpty ? jsonDecode(response.body) : {}; - - Log.info( - '${endpoint.name} request failed: ${response.statusCode}, $errorBody ', - ); - - return FlowyResult.failure( - FlowyError( - msg: errorBody['msg'] ?? errorMessage, - ), - ); - } - } catch (e) { - Log.error('${endpoint.name} request failed: error: $e'); - - return FlowyResult.failure( - FlowyError(msg: 'Network error: ${e.toString()}'), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 9691a1269b..6fda156567 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -30,26 +30,12 @@ class SignInBloc extends Bloc { on( (event, emit) async { await event.when( - signInWithEmailAndPassword: (email, password) async => - _onSignInWithEmailAndPassword( - emit, - email: email, - password: password, - ), - signInWithOAuth: (platform) async => _onSignInWithOAuth( - emit, - platform: platform, - ), - signInAsGuest: () async => _onSignInAsGuest(emit), - signInWithMagicLink: (email) async => _onSignInWithMagicLink( - emit, - email: email, - ), - signInWithPasscode: (email, passcode) async => _onSignInWithPasscode( - emit, - email: email, - passcode: passcode, - ), + signedInWithUserEmailAndPassword: () async => _onSignIn(emit), + signedInWithOAuth: (platform) async => + _onSignInWithOAuth(emit, platform), + signedInAsGuest: () async => _onSignInAsGuest(emit), + signedWithMagicLink: (email) async => + _onSignInWithMagicLink(emit, email), deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result), cancel: () { emit( @@ -133,34 +119,26 @@ class SignInBloc extends Bloc { } } - Future _onSignInWithEmailAndPassword( - Emitter emit, { - required String email, - required String password, - }) async { + Future _onSignIn(Emitter emit) async { final result = await authService.signInWithEmailPassword( - email: email, - password: password, + email: state.email ?? '', + password: state.password ?? '', ); emit( result.fold( - (gotrueTokenResponse) { - getIt().passGotrueTokenResponse( - gotrueTokenResponse, - ); - return state.copyWith( - isSubmitting: false, - ); - }, + (userProfile) => state.copyWith( + isSubmitting: false, + successOrFail: FlowyResult.success(userProfile), + ), (error) => _stateFromCode(error), ), ); } Future _onSignInWithOAuth( - Emitter emit, { - required String platform, - }) async { + Emitter emit, + String platform, + ) async { emit( state.copyWith( isSubmitting: true, @@ -183,16 +161,9 @@ class SignInBloc extends Bloc { } Future _onSignInWithMagicLink( - Emitter emit, { - required String email, - }) async { - if (state.isSubmitting) { - Log.error('Sign in with magic link is already in progress'); - return; - } - - Log.info('Sign in with magic link: $email'); - + Emitter emit, + String email, + ) async { emit( state.copyWith( isSubmitting: true, @@ -206,50 +177,7 @@ class SignInBloc extends Bloc { emit( result.fold( - (userProfile) => state.copyWith( - isSubmitting: false, - ), - (error) => _stateFromCode(error), - ), - ); - } - - Future _onSignInWithPasscode( - Emitter emit, { - required String email, - required String passcode, - }) async { - if (state.isSubmitting) { - Log.error('Sign in with passcode is already in progress'); - return; - } - - Log.info('Sign in with passcode: $email, $passcode'); - - emit( - state.copyWith( - isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, - ), - ); - - final result = await authService.signInWithPasscode( - email: email, - passcode: passcode, - ); - - emit( - result.fold( - (gotrueTokenResponse) { - getIt().passGotrueTokenResponse( - gotrueTokenResponse, - ); - return state.copyWith( - isSubmitting: false, - ); - }, + (userProfile) => state.copyWith(isSubmitting: true), (error) => _stateFromCode(error), ), ); @@ -296,20 +224,10 @@ class SignInBloc extends Bloc { emailError: null, ); case ErrorCode.UserUnauthorized: - final errorMsg = error.msg; - String msg = LocaleKeys.signIn_generalError.tr(); - if (errorMsg.contains('rate limit') || - errorMsg.contains('For security purposes')) { - msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); - } else if (errorMsg.contains('invalid')) { - msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); - } else if (errorMsg.contains('Invalid login credentials')) { - msg = LocaleKeys.signIn_invalidLoginCredentials.tr(); - } return state.copyWith( isSubmitting: false, successOrFail: FlowyResult.failure( - FlowyError(msg: msg), + FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()), ), ); default: @@ -325,35 +243,19 @@ class SignInBloc extends Bloc { @freezed class SignInEvent with _$SignInEvent { - // Sign in methods - const factory SignInEvent.signInWithEmailAndPassword({ - required String email, - required String password, - }) = SignInWithEmailAndPassword; - const factory SignInEvent.signInWithOAuth({ - required String platform, - }) = SignInWithOAuth; - const factory SignInEvent.signInAsGuest() = SignInAsGuest; - const factory SignInEvent.signInWithMagicLink({ - required String email, - }) = SignInWithMagicLink; - const factory SignInEvent.signInWithPasscode({ - required String email, - required String passcode, - }) = SignInWithPasscode; - - // Event handlers - const factory SignInEvent.emailChanged({ - required String email, - }) = EmailChanged; - const factory SignInEvent.passwordChanged({ - required String password, - }) = PasswordChanged; + const factory SignInEvent.signedInWithUserEmailAndPassword() = + SignedInWithUserEmailAndPassword; + const factory SignInEvent.signedInWithOAuth(String platform) = + SignedInWithOAuth; + const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; + const factory SignInEvent.signedWithMagicLink(String email) = + SignedWithMagicLink; + const factory SignInEvent.emailChanged(String email) = EmailChanged; + const factory SignInEvent.passwordChanged(String password) = PasswordChanged; const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) = DeepLinkStateChange; - - const factory SignInEvent.cancel() = Cancel; - const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType; + const factory SignInEvent.cancel() = _Cancel; + const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType; } // we support sign in directly without sign up, but we want to allow the users to sign up if they want to diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index d3ebe0201b..36d6039d40 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -24,7 +24,7 @@ typedef DidUpdateUserWorkspacesCallback = void Function( ); typedef UserProfileNotifyValue = FlowyResult; typedef DidUpdateUserWorkspaceSetting = void Function( - WorkspaceSettingsPB settings, + UseAISettingPB settings, ); class UserListener { @@ -101,10 +101,10 @@ class UserListener { result.map( (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)), ); - case user.UserNotification.DidUpdateWorkspaceSetting: + case user.UserNotification.DidUpdateAISetting: result.map( - (r) => onUserWorkspaceSettingUpdated - ?.call(WorkspaceSettingsPB.fromBuffer(r)), + (r) => + onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)), ); break; default: @@ -113,21 +113,22 @@ class UserListener { } } -typedef WorkspaceLatestNotifyValue = FlowyResult; +typedef WorkspaceSettingNotifyValue + = FlowyResult; class FolderListener { FolderListener(); - final PublishNotifier _latestChangedNotifier = + final PublishNotifier _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(WorkspaceLatestNotifyValue)? onLatestUpdated, + void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { - if (onLatestUpdated != null) { - _latestChangedNotifier.addPublishListener(onLatestUpdated); + if (onSettingUpdated != null) { + _settingChangedNotifier.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -145,9 +146,9 @@ class FolderListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _latestChangedNotifier.value = - FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)), - (error) => _latestChangedNotifier.value = FlowyResult.failure(error), + (payload) => _settingChangedNotifier.value = + FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), + (error) => _settingChangedNotifier.value = FlowyResult.failure(error), ); break; default: @@ -157,6 +158,6 @@ class FolderListener { Future stop() async { await _listener?.stop(); - _latestChangedNotifier.dispose(); + _settingChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index ff1cfb6575..5a75a4df3e 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -40,6 +40,8 @@ class UserBackendService implements IUserBackendService { String? password, String? email, String? iconUrl, + String? openAIKey, + String? stabilityAiKey, }) { final payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -59,6 +61,14 @@ class UserBackendService implements IUserBackendService { payload.iconUrl = iconUrl; } + if (openAIKey != null) { + payload.openaiKey = openAIKey; + } + + if (stabilityAiKey != null) { + payload.stabilityAiKey = stabilityAiKey; + } + return UserEventUpdateUserProfile(payload).send(); } @@ -76,26 +86,6 @@ class UserBackendService implements IUserBackendService { return UserEventMagicLinkSignIn(payload).send(); } - static Future> - signInWithPasscode( - String email, - String passcode, - ) async { - final payload = PasscodeSignInPB(email: email, passcode: passcode); - return UserEventPasscodeSignIn(payload).send(); - } - - Future> signInWithPassword( - String email, - String password, - ) { - final payload = SignInPayloadPB( - email: email, - password: password, - ); - return UserEventSignInWithEmailPassword(payload).send(); - } - static Future> signOut() { return UserEventSignOut().send(); } @@ -121,13 +111,8 @@ class UserBackendService implements IUserBackendService { }); } - Future> openWorkspace( - String workspaceId, - AuthTypePB authType, - ) { - final payload = OpenUserWorkspacePB() - ..workspaceId = workspaceId - ..workspaceAuthType = authType; + Future> openWorkspace(String workspaceId) { + final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId; return UserEventOpenWorkspace(payload).send(); } @@ -140,13 +125,25 @@ class UserBackendService implements IUserBackendService { }); } + Future> createWorkspace( + String name, + String desc, + ) { + final request = CreateWorkspacePayloadPB.create() + ..name = name + ..desc = desc; + return FolderEventCreateFolderWorkspace(request).send().then((result) { + return result.fold( + (workspace) => FlowyResult.success(workspace), + (error) => FlowyResult.failure(error), + ); + }); + } + Future> createUserWorkspace( String name, - AuthTypePB authType, ) { - final request = CreateWorkspacePB.create() - ..name = name - ..authType = authType; + final request = CreateWorkspacePB.create()..name = name; return UserEventCreateWorkspace(request).send(); } @@ -244,6 +241,13 @@ class UserBackendService implements IUserBackendService { return UserEventGetWorkspaceSubscriptionInfo(params).send(); } + Future> + getWorkspaceMember() async { + final data = WorkspaceMemberIdPB.create()..uid = userId; + + return UserEventGetMemberInfo(data).send(); + } + @override Future> createSubscription( String workspaceId, diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart index 7ff50dbd02..ce51fdd10b 100644 --- a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart @@ -1,7 +1,9 @@ import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,10 +22,20 @@ class WorkspaceErrorBloc void _dispatch() { on( (event, emit) async { - event.when( + await event.when( init: () { // _loadSnapshots(); }, + resetWorkspace: () async { + emit(state.copyWith(loadingState: const LoadingState.loading())); + final payload = ResetWorkspacePB.create() + ..workspaceId = userFolder.workspaceId + ..uid = userFolder.uid; + final result = await UserEventResetWorkspace(payload).send(); + if (!isClosed) { + add(WorkspaceErrorEvent.didResetWorkspace(result)); + } + }, didResetWorkspace: (result) { result.fold( (_) { @@ -56,6 +68,7 @@ class WorkspaceErrorBloc class WorkspaceErrorEvent with _$WorkspaceErrorEvent { const factory WorkspaceErrorEvent.init() = _Init; const factory WorkspaceErrorEvent.logout() = _DidLogout; + const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace; const factory WorkspaceErrorEvent.didResetWorkspace( FlowyResult result, ) = _DidResetWorkspace; diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index ddb1a07f96..a9b11cb42e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,8 +74,9 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.workspaceAuthType != AuthTypePB.Local; - final desc = "${user.name}\t ${user.workspaceAuthType}\t"; + final isDisabled = + isSelected || user.authenticator != AuthenticatorPB.Local; + final desc = "${user.name}\t ${user.authenticator}\t"; final child = SizedBox( height: 30, child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index ccad6c0a26..2e8e4feeae 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -17,12 +17,14 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { case ErrorCode.InvalidEncryptSecret: case ErrorCode.NetworkError: showToastNotification( + context, message: error.msg, type: ToastificationType.error, ); break; default: showToastNotification( + context, message: error.msg, type: ToastificationType.error, callbacks: ToastificationCallbacks( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart new file mode 100644 index 0000000000..9abd417df3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart @@ -0,0 +1,25 @@ +import 'package:appflowy/user/presentation/helpers/helpers.dart'; +import 'package:appflowy/user/presentation/presentation.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/material.dart'; + +void handleUserProfileResult( + FlowyResult userProfileResult, + BuildContext context, + AuthRouter authRouter, +) { + userProfileResult.fold( + (userProfile) { + if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { + authRouter.pushEncryptionScreen(context, userProfile); + } else { + authRouter.goHomeScreen(context, userProfile); + } + }, + (error) { + handleOpenWorkspaceError(context, error); + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart index 11f321232e..084a360666 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart @@ -1 +1,2 @@ export 'handle_open_workspace_error.dart'; +export 'handle_user_profile_result.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 339c2f29f7..370d9c2062 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -21,6 +21,10 @@ class AuthRouter { getIt().pushWorkspaceStartScreen(context, userProfile); } + void pushSignUpScreen(BuildContext context) { + context.push(SignUpScreen.routeName); + } + /// Navigates to the home screen based on the current workspace and platform. /// /// This function takes in a [BuildContext] and a [UserProfilePB] object to @@ -57,6 +61,20 @@ class AuthRouter { ); } + void pushEncryptionScreen( + BuildContext context, + UserProfilePB userProfile, + ) { + // After log in,push EncryptionScreen on the top SignInScreen + context.push( + EncryptSecretScreen.routeName, + extra: { + EncryptSecretScreen.argUser: userProfile, + EncryptSecretScreen.argKey: ValueKey(userProfile.id), + }, + ); + } + Future pushWorkspaceErrorScreen( BuildContext context, UserFolderPB userFolder, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart new file mode 100644 index 0000000000..f0b79ed9d2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/presentation/helpers/helpers.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../application/encrypt_secret_bloc.dart'; + +class EncryptSecretScreen extends StatefulWidget { + const EncryptSecretScreen({required this.user, super.key}); + + final UserProfilePB user; + + static const routeName = '/EncryptSecretScreen'; + + // arguments used in GoRouter + static const argUser = 'user'; + static const argKey = 'key'; + + @override + State createState() => _EncryptSecretScreenState(); +} + +class _EncryptSecretScreenState extends State { + final TextEditingController _textEditingController = TextEditingController(); + + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocProvider( + create: (context) => EncryptSecretBloc(user: widget.user), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => + previous.isSignOut != current.isSignOut, + listener: (context, state) async { + if (state.isSignOut) { + await runAppFlowy(); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.successOrFail != current.successOrFail, + listener: (context, state) async { + await state.successOrFail?.fold( + (unit) async { + await runAppFlowy(); + }, + (error) { + handleOpenWorkspaceError(context, error); + }, + ); + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + final indicator = state.loadingState?.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) => const SizedBox.shrink(), + idle: () => const SizedBox.shrink(), + ) ?? + const SizedBox.shrink(); + return Center( + child: SizedBox( + width: 300, + height: 160, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Opacity( + opacity: 0.6, + child: FlowyText.medium( + "${LocaleKeys.settings_menu_inputEncryptPrompt.tr()} ${widget.user.email}", + fontSize: 14, + maxLines: 10, + ), + ), + const VSpace(6), + SizedBox( + width: 300, + child: FlowyTextField( + controller: _textEditingController, + hintText: + LocaleKeys.settings_menu_inputTextFieldHint.tr(), + onChanged: (_) {}, + ), + ), + OkCancelButton( + alignment: MainAxisAlignment.end, + onOkPressed: () => + context.read().add( + EncryptSecretEvent.setEncryptSecret( + _textEditingController.text, + ), + ), + onCancelPressed: () => context + .read() + .add(const EncryptSecretEvent.cancelInputSecret()), + mode: TextButtonMode.normal, + ), + const VSpace(6), + indicator, + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 2aeba87995..088da38978 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -1,5 +1,7 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; +export 'sign_up_screen.dart'; +export 'encrypt_secret_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 40901e92e1..94b4347869 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -1,14 +1,11 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/settings/show_settings.dart'; import 'package:appflowy/shared/window_title_bar.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -22,11 +19,9 @@ class DesktopSignInScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - + const indicatorMinHeight = 4.0; return BlocBuilder( builder: (context, state) { - final bottomPadding = UniversalPlatform.isDesktop ? 20.0 : 24.0; return Scaffold( appBar: _buildAppBar(), body: Center( @@ -34,31 +29,39 @@ class DesktopSignInScreen extends StatelessWidget { children: [ const Spacer(), + const VSpace(20), + // logo and title FlowyLogoTitle( title: LocaleKeys.welcomeText.tr(), - logoSize: Size.square(36), + logoSize: const Size(60, 60), ), - VSpace(theme.spacing.xxl), + const VSpace(20), - // continue with email and password - isLocalAuthEnabled - ? const SignInAnonymousButtonV3() - : const ContinueWithEmailAndPassword(), - - VSpace(theme.spacing.xxl), + // magic link sign in + const SignInWithMagicLinkButtons(), + const VSpace(20), // third-party sign in. if (isAuthEnabled) ...[ const _OrDivider(), - VSpace(theme.spacing.xxl), + const VSpace(20), const ThirdPartySignInButtons(), - VSpace(theme.spacing.xxl), + const VSpace(20), ], // sign in agreement const SignInAgreement(), + // loading status + const VSpace(indicatorMinHeight), + state.isSubmitting + ? const LinearProgressIndicator( + minHeight: indicatorMinHeight, + ) + : const VSpace(indicatorMinHeight), + const VSpace(20), + const Spacer(), // anonymous sign in and settings @@ -66,11 +69,11 @@ class DesktopSignInScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ DesktopSignInSettingsButton(), - HSpace(20), + HSpace(42), SignInAnonymousButtonV2(), ], ), - VSpace(bottomPadding), + const VSpace(16), ], ), ), @@ -96,24 +99,18 @@ class DesktopSignInSettingsButton extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFGhostIconTextButton( - text: LocaleKeys.signIn_settings.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, + return FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.signIn_settings.tr(), + textAlign: TextAlign.center, + fontSize: 12.0, + // fontWeight: FontWeight.w500, + color: Colors.grey, + decoration: TextDecoration.underline, ), - onTap: () => showSimpleSettingsDialog(context), - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.settings_s, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); + onTap: () { + showSimpleSettingsDialog(context); }, ); } @@ -124,30 +121,14 @@ class _OrDivider extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Row( children: [ - Flexible( - child: Divider( - thickness: 1, - color: theme.borderColorScheme.greyTertiary, - ), - ), + const Flexible(child: Divider(thickness: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - LocaleKeys.signIn_or.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), - ), - Flexible( - child: Divider( - thickness: 1, - color: theme.borderColorScheme.greyTertiary, - ), + child: FlowyText.regular(LocaleKeys.signIn_or.tr()), ), + const Flexible(child: Divider(thickness: 1)), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 9eb7d5a965..863aadc49c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -5,10 +5,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/user/presentation/widgets/flowy_logo_title.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -22,29 +19,32 @@ class MobileSignInScreen extends StatelessWidget { @override Widget build(BuildContext context) { + const double spacing = 16; + final colorScheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { - final theme = AppFlowyTheme.of(context); return Scaffold( resizeToAvoidBottomInset: false, body: Padding( padding: const EdgeInsets.symmetric(vertical: 38, horizontal: 40), child: Column( children: [ - const Spacer(), - FlowyLogoTitle(title: LocaleKeys.welcomeText.tr()), - VSpace(theme.spacing.xxl), - isLocalAuthEnabled - ? const SignInAnonymousButtonV3() - : const ContinueWithEmailAndPassword(), - VSpace(theme.spacing.xxl), - if (isAuthEnabled) ...[ - _buildThirdPartySignInButtons(context), - VSpace(theme.spacing.xxl), - ], + const Spacer(flex: 4), + _buildLogo(), + const VSpace(spacing), + _buildAppNameText(colorScheme), + const VSpace(spacing * 2), + const SignInWithMagicLinkButtons(), + const VSpace(spacing), + if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), + const VSpace(spacing * 1.5), const SignInAgreement(), + const VSpace(spacing), + if (!isAuthEnabled) const Spacer(flex: 2), + const Spacer(flex: 2), const Spacer(), - _buildSettingsButton(context), + Expanded(child: _buildSettingsButton(context)), + if (Platform.isAndroid) const Spacer(), ], ), ), @@ -53,8 +53,25 @@ class MobileSignInScreen extends StatelessWidget { ); } - Widget _buildThirdPartySignInButtons(BuildContext context) { - final theme = AppFlowyTheme.of(context); + Widget _buildLogo() { + return const FlowySvg( + FlowySvgs.flowy_logo_xl, + size: Size.square(56), + blendMode: null, + ); + } + + Widget _buildAppNameText(ColorScheme colorScheme) { + return FlowyText( + LocaleKeys.appName.tr(), + textAlign: TextAlign.center, + fontSize: 28, + color: const Color(0xFF00BCF0), + fontWeight: FontWeight.w700, + ); + } + + Widget _buildThirdPartySignInButtons(ColorScheme colorScheme) { return Column( children: [ Row( @@ -63,12 +80,10 @@ class MobileSignInScreen extends StatelessWidget { const Expanded(child: Divider()), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( + child: FlowyText( LocaleKeys.signIn_or.tr(), - style: TextStyle( - fontSize: 16, - color: theme.textColorScheme.secondary, - ), + fontSize: 12, + color: colorScheme.onSecondary, ), ), const Expanded(child: Divider()), @@ -85,34 +100,25 @@ class MobileSignInScreen extends StatelessWidget { } Widget _buildSettingsButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( mainAxisSize: MainAxisSize.min, children: [ - AFGhostIconTextButton( - text: LocaleKeys.signIn_settings.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.signIn_settings.tr(), + textAlign: TextAlign.center, + fontSize: 12.0, + // fontWeight: FontWeight.w500, + color: Colors.grey, + decoration: TextDecoration.underline, ), - onTap: () => context.push(MobileLaunchSettingsPage.routeName), - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.settings_s, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); + onTap: () { + context.push(MobileLaunchSettingsPage.routeName); }, ), const HSpace(24), - isLocalAuthEnabled - ? const ChangeCloudModeButton() - : const SignInAnonymousButtonV2(), + const SignInAnonymousButtonV2(), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index b359b2e217..5b99ad83f3 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -2,12 +2,14 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:universal_platform/universal_platform.dart'; +import '../../helpers/helpers.dart'; + class SignInScreen extends StatelessWidget { const SignInScreen({super.key}); @@ -20,9 +22,13 @@ class SignInScreen extends StatelessWidget { child: BlocConsumer( listener: _showSignInError, builder: (context, state) { - return UniversalPlatform.isDesktop - ? const DesktopSignInScreen() - : const MobileSignInScreen(); + final isLoading = context.read().state.isSubmitting; + if (UniversalPlatform.isMobile) { + return isLoading + ? const MobileLoadingScreen() + : const MobileSignInScreen(); + } + return const DesktopSignInScreen(); }, ), ); @@ -31,13 +37,10 @@ class SignInScreen extends StatelessWidget { void _showSignInError(BuildContext context, SignInState state) { final successOrFail = state.successOrFail; if (successOrFail != null) { - successOrFail.fold( - (userProfile) { - getIt().goHomeScreen(context, userProfile); - }, - (error) { - Log.error('Sign in error: $error'); - }, + handleUserProfileResult( + successOrFail, + context, + getIt(), ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart deleted file mode 100644 index a7a1b9722d..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/anon_user_bloc.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SignInAnonymousButtonV3 extends StatelessWidget { - const SignInAnonymousButtonV3({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, signInState) { - return BlocProvider( - create: (context) => AnonUserBloc() - ..add( - const AnonUserEvent.initial(), - ), - child: BlocListener( - listener: (context, state) async { - if (state.openedAnonUser != null) { - await runAppFlowy(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final text = LocaleKeys.signIn_continueWithLocalModel.tr(); - final onTap = state.anonUsers.isEmpty - ? () { - context - .read() - .add(const SignInEvent.signInAsGuest()); - } - : () { - final bloc = context.read(); - final user = bloc.state.anonUsers.first; - bloc.add(AnonUserEvent.openAnonUser(user)); - }; - return AFFilledTextButton.primary( - text: text, - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - }, - ), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart deleted file mode 100644 index 351527137f..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class AnonymousSignInButton extends StatelessWidget { - const AnonymousSignInButton({super.key}); - - @override - Widget build(BuildContext context) { - return AFGhostButton.normal( - onTap: () {}, - builder: (context, isHovering, disabled) { - return const Placeholder(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart deleted file mode 100644 index c4cf504ef5..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class ContinueWithEmail extends StatelessWidget { - const ContinueWithEmail({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AFFilledTextButton.primary( - text: LocaleKeys.signIn_continueWithEmail.tr(), - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart deleted file mode 100644 index 5027874418..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; - -class ContinueWithEmailAndPassword extends StatefulWidget { - const ContinueWithEmailAndPassword({super.key}); - - @override - State createState() => - _ContinueWithEmailAndPasswordState(); -} - -class _ContinueWithEmailAndPasswordState - extends State { - final controller = TextEditingController(); - final focusNode = FocusNode(); - final emailKey = GlobalKey(); - - bool _hasPushedContinueWithMagicLinkOrPasscodePage = false; - - @override - void dispose() { - controller.dispose(); - focusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - // only push the continue with magic link or passcode page if the magic link is sent successfully - if (successOrFail != null) { - successOrFail.fold( - (_) => emailKey.currentState?.clearError(), - (error) => emailKey.currentState?.syncError( - errorText: error.msg, - ), - ); - } else if (successOrFail == null && !state.isSubmitting) { - emailKey.currentState?.clearError(); - } - }, - child: Column( - children: [ - AFTextField( - key: emailKey, - controller: controller, - hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), - onSubmitted: (value) => _signInWithEmail( - context, - value, - ), - ), - VSpace(theme.spacing.l), - ContinueWithEmail( - onTap: () => _signInWithEmail( - context, - controller.text, - ), - ), - VSpace(theme.spacing.l), - ContinueWithPassword( - onTap: () { - final email = controller.text; - - if (!isEmail(email)) { - emailKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidEmail.tr(), - ); - return; - } - - _pushContinueWithPasswordPage( - context, - email, - ); - }, - ), - ], - ), - ); - } - - void _signInWithEmail(BuildContext context, String email) { - if (!isEmail(email)) { - emailKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidEmail.tr(), - ); - return; - } - - context - .read() - .add(SignInEvent.signInWithMagicLink(email: email)); - - _pushContinueWithMagicLinkOrPasscodePage( - context, - email, - ); - } - - void _pushContinueWithMagicLinkOrPasscodePage( - BuildContext context, - String email, - ) { - if (_hasPushedContinueWithMagicLinkOrPasscodePage) { - return; - } - - final signInBloc = context.read(); - - // push the a continue with magic link or passcode screen - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: signInBloc, - child: ContinueWithMagicLinkOrPasscodePage( - email: email, - backToLogin: () { - Navigator.pop(context); - - emailKey.currentState?.clearError(); - - _hasPushedContinueWithMagicLinkOrPasscodePage = false; - }, - onEnterPasscode: (passcode) { - signInBloc.add( - SignInEvent.signInWithPasscode( - email: email, - passcode: passcode, - ), - ); - }, - ), - ), - ), - ); - - _hasPushedContinueWithMagicLinkOrPasscodePage = true; - } - - void _pushContinueWithPasswordPage( - BuildContext context, - String email, - ) { - final signInBloc = context.read(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: signInBloc, - child: ContinueWithPasswordPage( - email: email, - backToLogin: () { - emailKey.currentState?.clearError(); - Navigator.pop(context); - }, - onEnterPassword: (password) => signInBloc.add( - SignInEvent.signInWithEmailAndPassword( - email: email, - password: password, - ), - ), - onForgotPassword: () { - // todo: implement forgot password - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart deleted file mode 100644 index c29a18ea30..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ContinueWithMagicLinkOrPasscodePage extends StatefulWidget { - const ContinueWithMagicLinkOrPasscodePage({ - super.key, - required this.backToLogin, - required this.email, - required this.onEnterPasscode, - }); - - final String email; - final VoidCallback backToLogin; - final ValueChanged onEnterPasscode; - - @override - State createState() => - _ContinueWithMagicLinkOrPasscodePageState(); -} - -class _ContinueWithMagicLinkOrPasscodePageState - extends State { - final passcodeController = TextEditingController(); - - bool isEnteringPasscode = false; - - ToastificationItem? toastificationItem; - - final inputPasscodeKey = GlobalKey(); - - bool isSubmitting = false; - - @override - void dispose() { - passcodeController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null && successOrFail.isFailure) { - successOrFail.onFailure((error) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(), - ); - }); - } - - if (state.isSubmitting != isSubmitting) { - setState(() => isSubmitting = state.isSubmitting); - } - }, - child: Scaffold( - body: Center( - child: SizedBox( - width: 320, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo, title and description - ..._buildLogoTitleAndDescription(), - - // Enter code manually - ..._buildEnterCodeManually(), - - // Back to login - ..._buildBackToLogin(), - ], - ), - ), - ), - ), - ); - } - - List _buildEnterCodeManually() { - // todo: ask designer to provide the spacing - final spacing = VSpace(20); - final textStyle = AFButtonSize.l.buildTextStyle(context); - final textHeight = textStyle.height; - final textFontSize = textStyle.fontSize; - - // the indicator height is the height of the text style. - double indicatorHeight = 20; - if (textHeight != null && textFontSize != null) { - indicatorHeight = textHeight * textFontSize; - } - - if (!isEnteringPasscode) { - return [ - AFFilledTextButton.primary( - text: LocaleKeys.signIn_enterCodeManually.tr(), - onTap: () => setState(() => isEnteringPasscode = true), - size: AFButtonSize.l, - alignment: Alignment.center, - ), - spacing, - ]; - } - - return [ - // Enter code manually - AFTextField( - key: inputPasscodeKey, - controller: passcodeController, - hintText: LocaleKeys.signIn_enterCode.tr(), - keyboardType: TextInputType.number, - autoFocus: true, - onSubmitted: (passcode) { - if (passcode.isEmpty) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), - ); - } else { - widget.onEnterPasscode(passcode); - } - }, - ), - // todo: ask designer to provide the spacing - VSpace(12), - - // continue to login - !isSubmitting - ? _buildContinueButton(textStyle: textStyle) - : _buildIndicator(indicatorHeight: indicatorHeight), - - spacing, - ]; - } - - Widget _buildContinueButton({ - required TextStyle textStyle, - }) { - return AFFilledTextButton.primary( - text: LocaleKeys.signIn_continueToSignIn.tr(), - onTap: () { - final passcode = passcodeController.text; - if (passcode.isEmpty) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), - ); - } else { - widget.onEnterPasscode(passcode); - } - }, - textStyle: textStyle.copyWith( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - size: AFButtonSize.l, - alignment: Alignment.center, - ); - } - - Widget _buildIndicator({ - required double indicatorHeight, - }) { - return AFFilledButton.disabled( - size: AFButtonSize.l, - builder: (context, isHovering, disabled) { - return Align( - child: SizedBox.square( - dimension: indicatorHeight, - child: CircularProgressIndicator( - strokeWidth: 3.0, - ), - ), - ); - }, - ); - } - - List _buildBackToLogin() { - return [ - AFGhostTextButton( - text: LocaleKeys.signIn_backToLogin.tr(), - size: AFButtonSize.s, - onTap: widget.backToLogin, - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ]; - } - - List _buildLogoTitleAndDescription() { - final theme = AppFlowyTheme.of(context); - final spacing = VSpace(theme.spacing.xxl); - if (!isEnteringPasscode) { - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // description - Text( - LocaleKeys.signIn_temporaryVerificationLinkSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } else { - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_enterCode.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // description - Text( - LocaleKeys.signIn_temporaryVerificationCodeSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart deleted file mode 100644 index 5bfd191e22..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ContinueWithPassword extends StatelessWidget { - const ContinueWithPassword({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AFOutlinedTextButton.normal( - text: 'Continue with password', - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart deleted file mode 100644 index 1e2ed6e100..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ContinueWithPasswordPage extends StatefulWidget { - const ContinueWithPasswordPage({ - super.key, - required this.backToLogin, - required this.email, - required this.onEnterPassword, - required this.onForgotPassword, - }); - - final String email; - final VoidCallback backToLogin; - final ValueChanged onEnterPassword; - final VoidCallback onForgotPassword; - - @override - State createState() => - _ContinueWithPasswordPageState(); -} - -class _ContinueWithPasswordPageState extends State { - final passwordController = TextEditingController(); - final inputPasswordKey = GlobalKey(); - - @override - void dispose() { - passwordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: SizedBox( - width: 320, - child: BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null && successOrFail.isFailure) { - successOrFail.onFailure((error) { - inputPasswordKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), - ); - }); - } else if (state.passwordError != null) { - inputPasswordKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), - ); - } else { - inputPasswordKey.currentState?.clearError(); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo and title - ..._buildLogoAndTitle(), - - // Password input and buttons - ..._buildPasswordSection(), - - // Back to login - ..._buildBackToLogin(), - ], - ), - ), - ), - ), - ); - } - - List _buildLogoAndTitle() { - final theme = AppFlowyTheme.of(context); - final spacing = VSpace(theme.spacing.xxl); - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_enterPassword.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // email display - RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.signIn_loginAs.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: ' ${widget.email}', - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - ], - ), - ), - spacing, - ]; - } - - List _buildPasswordSection() { - final theme = AppFlowyTheme.of(context); - final iconSize = 20.0; - return [ - // Password input - AFTextField( - key: inputPasswordKey, - controller: passwordController, - hintText: LocaleKeys.signIn_enterPassword.tr(), - autoFocus: true, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - inputPasswordKey.currentState?.syncObscured(!isObscured); - }, - ), - onSubmitted: widget.onEnterPassword, - ), - // todo: ask designer to provide the spacing - VSpace(8), - - // Forgot password button - Align( - alignment: Alignment.centerLeft, - child: AFGhostTextButton( - text: LocaleKeys.signIn_forgotPassword.tr(), - size: AFButtonSize.s, - padding: EdgeInsets.zero, - onTap: widget.onForgotPassword, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ), - VSpace(20), - - // Continue button - AFFilledTextButton.primary( - text: LocaleKeys.web_continue.tr(), - onTap: () => widget.onEnterPassword(passwordController.text), - size: AFButtonSize.l, - alignment: Alignment.center, - ), - VSpace(20), - ]; - } - - List _buildBackToLogin() { - return [ - AFGhostTextButton( - text: LocaleKeys.signIn_backToLogin.tr(), - size: AFButtonSize.s, - onTap: widget.backToLogin, - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart deleted file mode 100644 index 8e126db7ad..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flutter/material.dart'; - -class AFLogo extends StatelessWidget { - const AFLogo({ - super.key, - this.size = const Size.square(36), - }); - - final Size size; - - @override - Widget build(BuildContext context) { - return FlowySvg( - FlowySvgs.app_logo_xl, - blendMode: null, - size: size, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 45e4fe7273..0486d67838 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -64,16 +64,14 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { - showToastNotification( + return showToastNotification( + context, message: LocaleKeys.signIn_invalidEmail.tr(), type: ToastificationType.error, ); - return; } - context - .read() - .add(SignInEvent.signInWithMagicLink(email: email)); + context.read().add(SignInEvent.signedWithMagicLink(email)); showConfirmDialog( context: context, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index 76ce87ffc1..7351871b6a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -1,6 +1,5 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -12,38 +11,39 @@ class SignInAgreement extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final textStyle = theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ); - final underlinedTextStyle = theme.textStyle.caption.underline( - color: theme.textColorScheme.secondary, - ); return RichText( textAlign: TextAlign.center, text: TextSpan( children: [ TextSpan( - text: LocaleKeys.web_signInAgreement.tr(), - style: textStyle, + text: '${LocaleKeys.web_signInAgreement.tr()} ', + style: const TextStyle(color: Colors.grey, fontSize: 12), ), TextSpan( text: '${LocaleKeys.web_termOfUse.tr()} ', - style: underlinedTextStyle, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + decoration: TextDecoration.underline, + ), mouseCursor: SystemMouseCursors.click, recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.com/terms'), + ..onTap = () => afLaunchUrlString('https://appflowy.io/terms'), ), TextSpan( text: '${LocaleKeys.web_and.tr()} ', - style: textStyle, + style: const TextStyle(color: Colors.grey, fontSize: 12), ), TextSpan( text: LocaleKeys.web_privacyPolicy.tr(), - style: underlinedTextStyle, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + decoration: TextDecoration.underline, + ), mouseCursor: SystemMouseCursors.click, recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.com/privacy'), + ..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'), ), ], ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index 33ef1d7bb0..bce22a714d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -1,13 +1,90 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:universal_platform/universal_platform.dart'; + +/// Used in DesktopSignInScreen and MobileSignInScreen +class SignInAnonymousButton extends StatelessWidget { + const SignInAnonymousButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final isMobile = UniversalPlatform.isMobile; + + return BlocBuilder( + builder: (context, signInState) { + return BlocProvider( + create: (context) => AnonUserBloc() + ..add( + const AnonUserEvent.initial(), + ), + child: BlocListener( + listener: (context, state) async { + if (state.openedAnonUser != null) { + await runAppFlowy(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final text = state.anonUsers.isEmpty + ? LocaleKeys.signIn_loginStartWithAnonymous.tr() + : LocaleKeys.signIn_continueAnonymousUser.tr(); + final onTap = state.anonUsers.isEmpty + ? () { + context + .read() + .add(const SignInEvent.signedInAsGuest()); + } + : () { + final bloc = context.read(); + final user = bloc.state.anonUsers.first; + bloc.add(AnonUserEvent.openAnonUser(user)); + }; + // SignInAnonymousButton in mobile + if (isMobile) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + ), + onPressed: onTap, + child: FlowyText( + LocaleKeys.signIn_loginStartWithAnonymous.tr(), + fontSize: 14, + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ); + } + // SignInAnonymousButton in desktop + return SizedBox( + height: 48, + child: FlowyButton( + isSelected: true, + disable: signInState.isSubmitting, + text: FlowyText.medium( + text, + textAlign: TextAlign.center, + ), + radius: Corners.s6Border, + onTap: onTap, + ), + ); + }, + ), + ), + ); + }, + ); + } +} class SignInAnonymousButtonV2 extends StatelessWidget { const SignInAnonymousButtonV2({ @@ -31,35 +108,27 @@ class SignInAnonymousButtonV2 extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - final theme = AppFlowyTheme.of(context); + final text = LocaleKeys.signIn_anonymous.tr(); final onTap = state.anonUsers.isEmpty ? () { context .read() - .add(const SignInEvent.signInAsGuest()); + .add(const SignInEvent.signedInAsGuest()); } : () { final bloc = context.read(); final user = bloc.state.anonUsers.first; bloc.add(AnonUserEvent.openAnonUser(user)); }; - return AFGhostIconTextButton( - text: LocaleKeys.signIn_anonymousMode.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, - ), - size: AFButtonSize.s, + return FlowyButton( + useIntrinsicWidth: true, onTap: onTap, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.anonymous_mode_m, - color: theme.textColorScheme.secondary, - ); - }, + text: FlowyText( + text, + color: Colors.grey, + decoration: TextDecoration.underline, + fontSize: 12, + ), ); }, ), @@ -69,39 +138,3 @@ class SignInAnonymousButtonV2 extends StatelessWidget { ); } } - -class ChangeCloudModeButton extends StatelessWidget { - const ChangeCloudModeButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFGhostIconTextButton( - text: LocaleKeys.signIn_switchToAppFlowyCloud.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, - ), - onTap: () async { - await useAppFlowyBetaCloudWithURL( - kAppflowyCloudUrl, - AuthenticatorType.appflowyCloud, - ); - await runAppFlowy(); - }, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.cloud_mode_m, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart index 7067844500..5146e29962 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class MobileLogoutButton extends StatelessWidget { @@ -18,19 +18,50 @@ class MobileLogoutButton extends StatelessWidget { @override Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: text, + final style = Theme.of(context); + return GestureDetector( onTap: onPressed, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - if (icon == null) { - return const SizedBox.shrink(); - } - return FlowySvg( - icon!, - size: Size.square(18), - ); - }, + child: Container( + height: 38, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all( + color: textColor ?? style.colorScheme.outline, + width: 0.5, + ), + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + SizedBox( + // The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space. + width: 30, + height: 30, + child: Center( + child: SizedBox( + width: 24, + child: FlowySvg( + icon!, + blendMode: null, + ), + ), + ), + ), + const HSpace(8), + ], + FlowyText( + text, + fontSize: 14.0, + fontWeight: FontWeight.w400, + color: textColor, + ), + ], + ), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart similarity index 53% rename from frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart rename to frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart index 9a7234ab6b..35d16b031f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button.dart @@ -1,7 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; +import 'package:appflowy/user/presentation/widgets/widgets.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; enum ThirdPartySignInButtonType { @@ -99,55 +102,118 @@ class MobileThirdPartySignInButton extends StatelessWidget { super.key, this.height = 38, this.fontSize = 14.0, - required this.onTap, + required this.onPressed, required this.type, }); - final VoidCallback onTap; + final VoidCallback onPressed; final double height; final double fontSize; final ThirdPartySignInButtonType type; @override Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: type.labelText, - onTap: onTap, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - type.icon, - size: Size.square(16), - blendMode: type.blendMode, - ); - }, + final style = Theme.of(context); + + return AnimatedGestureDetector( + scaleFactor: 1.0, + onTapUp: onPressed, + child: Container( + height: height, + decoration: BoxDecoration( + color: type.backgroundColor(context), + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all( + color: style.colorScheme.outline, + width: 0.5, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (type != ThirdPartySignInButtonType.anonymous) + FlowySvg( + type.icon, + size: Size.square(fontSize), + blendMode: type.blendMode, + color: type.textColor(context), + ), + const HSpace(8.0), + FlowyText( + type.labelText, + fontSize: fontSize, + color: type.textColor(context), + ), + ], + ), + ), ); } } -class DesktopThirdPartySignInButton extends StatelessWidget { - const DesktopThirdPartySignInButton({ +class DesktopSignInButton extends StatelessWidget { + const DesktopSignInButton({ super.key, required this.type, - required this.onTap, + required this.onPressed, }); final ThirdPartySignInButtonType type; - final VoidCallback onTap; + final VoidCallback onPressed; @override Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: type.labelText, - onTap: onTap, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - type.icon, - size: Size.square(18), - blendMode: type.blendMode, - ); - }, + final style = Theme.of(context); + // In desktop, the width of button is limited by [AuthFormContainer] + return SizedBox( + height: 48, + width: AuthFormContainer.width, + child: OutlinedButton.icon( + // In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left. + icon: Container( + width: AuthFormContainer.width / 4, + alignment: Alignment.centerRight, + child: SizedBox( + // Some icons are not square, so we just use a fixed width here. + width: 24, + child: FlowySvg( + type.icon, + blendMode: type.blendMode, + ), + ), + ), + label: Container( + padding: const EdgeInsets.only(left: 8), + alignment: Alignment.centerLeft, + child: FlowyText( + type.labelText, + fontSize: 14, + ), + ), + style: ButtonStyle( + overlayColor: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.hovered)) { + return style.colorScheme.onSecondaryContainer; + } + return null; + }, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: Corners.s6Border, + ), + ), + side: WidgetStateProperty.all( + BorderSide( + color: style.dividerColor, + ), + ), + ), + onPressed: onPressed, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart similarity index 67% rename from frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart rename to frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index 8d27846c46..7baa243e5f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -1,7 +1,8 @@ import 'dart:io'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -39,7 +40,7 @@ class ThirdPartySignInButtons extends StatelessWidget { void _signIn(BuildContext context, String provider) { context.read().add( - SignInEvent.signInWithOAuth(platform: provider), + SignInEvent.signedInWithOAuth(provider), ); } } @@ -57,22 +58,23 @@ class _DesktopThirdPartySignIn extends StatefulWidget { } class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { + static const padding = 12.0; + bool isExpanded = false; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Column( children: [ - DesktopThirdPartySignInButton( + DesktopSignInButton( key: signInWithGoogleButtonKey, type: ThirdPartySignInButtonType.google, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), ), - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( + const VSpace(padding), + DesktopSignInButton( type: ThirdPartySignInButtonType.apple, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), ), ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), ], @@ -80,39 +82,38 @@ class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { } List _buildExpandedButtons() { - final theme = AppFlowyTheme.of(context); return [ - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( + const VSpace(padding * 1.5), + DesktopSignInButton( type: ThirdPartySignInButtonType.github, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), ), - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( + const VSpace(padding), + DesktopSignInButton( type: ThirdPartySignInButtonType.discord, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), ), ]; } List _buildCollapsedButtons() { - final theme = AppFlowyTheme.of(context); return [ - VSpace(theme.spacing.l), - AFGhostTextButton( - text: 'More options', - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, + const VSpace(padding), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + isExpanded = !isExpanded; + }); + }, + child: FlowyText( + LocaleKeys.signIn_continueAnotherWay.tr(), + color: Theme.of(context).colorScheme.onSurface, + decoration: TextDecoration.underline, + fontSize: 14, + ), + ), ), ]; } @@ -152,14 +153,14 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { if (Platform.isIOS) ...[ MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.apple, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.apple), ), const VSpace(padding), ], MobileThirdPartySignInButton( key: signInWithGoogleButtonKey, type: ThirdPartySignInButtonType.google, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google), ), ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), ], @@ -171,33 +172,31 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { const VSpace(padding), MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.github, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.github), ), const VSpace(padding), MobileThirdPartySignInButton( type: ThirdPartySignInButtonType.discord, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), + onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.discord), ), ]; } List _buildCollapsedButtons() { - final theme = AppFlowyTheme.of(context); return [ const VSpace(padding * 2), - AFGhostTextButton( - text: 'More options', - textColor: (context, isHovering, disabled) { - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, + GestureDetector( onTap: () { setState(() { isExpanded = !isExpanded; }); }, + child: FlowyText( + LocaleKeys.signIn_continueAnotherWay.tr(), + color: Theme.of(context).colorScheme.onSurface, + decoration: TextDecoration.underline, + fontSize: 14, + ), ), ]; } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 6d79b896c1..18e260a472 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,7 +1,7 @@ -export 'continue_with/continue_with_email_and_password.dart'; -export 'sign_in_agreement.dart'; +export 'magic_link_sign_in_buttons.dart'; export 'sign_in_anonymous_button.dart'; export 'sign_in_or_logout_button.dart'; -export 'third_party_sign_in_button/third_party_sign_in_button.dart'; +export 'third_party_sign_in_button.dart'; // export 'switch_sign_in_sign_up_button.dart'; -export 'third_party_sign_in_button/third_party_sign_in_buttons.dart'; +export 'third_party_sign_in_buttons.dart'; +export 'sign_in_agreement.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart new file mode 100644 index 0000000000..8aea8dde55 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart @@ -0,0 +1,220 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/sign_up_bloc.dart'; +import 'package:appflowy/user/presentation/router.dart'; +import 'package:appflowy/user/presentation/widgets/widgets.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SignUpScreen extends StatelessWidget { + const SignUpScreen({ + super.key, + required this.router, + }); + + static const routeName = '/SignUpScreen'; + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: BlocListener( + listener: (context, state) { + final successOrFail = state.successOrFail; + if (successOrFail != null) { + _handleSuccessOrFail(context, successOrFail); + } + }, + child: const Scaffold(body: SignUpForm()), + ), + ); + } + + void _handleSuccessOrFail( + BuildContext context, + FlowyResult result, + ) { + result.fold( + (user) => router.pushWorkspaceStartScreen(context, user), + (error) => showSnapBar(context, error.msg), + ); + } +} + +class SignUpForm extends StatelessWidget { + const SignUpForm({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Align( + child: AuthFormContainer( + children: [ + FlowyLogoTitle( + title: LocaleKeys.signUp_title.tr(), + logoSize: const Size(60, 60), + ), + const VSpace(30), + const EmailTextField(), + const VSpace(5), + const PasswordTextField(), + const VSpace(5), + const RepeatPasswordTextField(), + const VSpace(30), + const SignUpButton(), + const VSpace(10), + const SignUpPrompt(), + if (context.read().state.isSubmitting) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(), + ], + ], + ), + ); + } +} + +class SignUpPrompt extends StatelessWidget { + const SignUpPrompt({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText.medium( + LocaleKeys.signUp_alreadyHaveAnAccount.tr(), + color: Theme.of(context).hintColor, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () => Navigator.pop(context), + child: FlowyText.medium( + LocaleKeys.signIn_buttonText.tr(), + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ); + } +} + +class SignUpButton extends StatelessWidget { + const SignUpButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.signUp_getStartedText.tr(), + height: 48, + onPressed: () { + context + .read() + .add(const SignUpEvent.signUpWithUserEmailAndPassword()); + }, + ); + } +} + +class PasswordTextField extends StatelessWidget { + const PasswordTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.passwordError != current.passwordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: const FlowySvg(FlowySvgs.hide_m), + obscureHideIcon: const FlowySvg(FlowySvgs.show_m), + hintText: LocaleKeys.signUp_passwordHint.tr(), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.passwordError ?? '', + onChanged: (value) => context + .read() + .add(SignUpEvent.passwordChanged(value)), + ); + }, + ); + } +} + +class RepeatPasswordTextField extends StatelessWidget { + const RepeatPasswordTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.repeatPasswordError != current.repeatPasswordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: const FlowySvg(FlowySvgs.hide_m), + obscureHideIcon: const FlowySvg(FlowySvgs.show_m), + hintText: LocaleKeys.signUp_repeatPasswordHint.tr(), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.repeatPasswordError ?? '', + onChanged: (value) => context + .read() + .add(SignUpEvent.repeatPasswordChanged(value)), + ); + }, + ); + } +} + +class EmailTextField extends StatelessWidget { + const EmailTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.emailError != current.emailError, + builder: (context, state) { + return RoundedInputField( + hintText: LocaleKeys.signUp_emailHint.tr(), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.emailError ?? '', + onChanged: (value) => + context.read().add(SignUpEvent.emailChanged(value)), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 4062cedf8e..146bf06df1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -8,6 +8,7 @@ import 'package:appflowy/user/presentation/helpers/helpers.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -60,15 +61,32 @@ class SplashScreen extends StatelessWidget { BuildContext context, Authenticated authenticated, ) async { - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (workspaceSetting) { - // After login, replace Splash screen by corresponding home screen - getIt().goHomeScreen( - context, - ); + final userProfile = authenticated.userProfile; + + /// After a user is authenticated, this function checks if encryption is required. + final result = await UserEventCheckEncryptionSign().send(); + await result.fold( + (check) async { + /// If encryption is needed, the user is navigated to the encryption screen. + /// Otherwise, it fetches the current workspace for the user and navigates them + if (check.requireSecret) { + getIt().pushEncryptionScreen(context, userProfile); + } else { + final result = await FolderEventGetCurrentWorkspaceSetting().send(); + result.fold( + (workspaceSetting) { + // After login, replace Splash screen by corresponding home screen + getIt().goHomeScreen( + context, + ); + }, + (error) => handleOpenWorkspaceError(context, error), + ); + } + }, + (err) { + Log.error(err); }, - (error) => handleOpenWorkspaceError(context, error), ); } @@ -97,7 +115,7 @@ class Body extends StatelessWidget { return Container( alignment: Alignment.center, child: UniversalPlatform.isMobile - ? const FlowySvg(FlowySvgs.app_logo_xl, blendMode: null) + ? const FlowySvg(FlowySvgs.flowy_logo_xl, blendMode: null) : const _DesktopSplashBody(), ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index af6d4ad770..d79127e04c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -85,6 +86,7 @@ class WorkspaceErrorScreen extends StatelessWidget { const VSpace(50), const LogoutButton(), const VSpace(20), + const ResetWorkspaceButton(), ]); return Center( @@ -155,3 +157,43 @@ class LogoutButton extends StatelessWidget { ); } } + +class ResetWorkspaceButton extends StatelessWidget { + const ResetWorkspaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 40, + child: BlocBuilder( + builder: (context, state) { + final isLoading = state.loadingState?.isLoading() ?? false; + final icon = isLoading + ? const Center( + child: CircularProgressIndicator.adaptive(), + ) + : null; + + return FlowyButton( + text: FlowyText.medium( + LocaleKeys.workspace_reset.tr(), + textAlign: TextAlign.center, + ), + onTap: () { + NavigatorAlertDialog( + title: LocaleKeys.workspace_resetWorkspacePrompt.tr(), + confirm: () { + context.read().add( + const WorkspaceErrorEvent.resetWorkspace(), + ); + }, + ).show(context); + }, + rightIcon: icon, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index a6124da60b..59b61aa54b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -57,7 +57,7 @@ class _MobileWorkspaceStartScreenState children: [ const Spacer(), const FlowySvg( - FlowySvgs.app_logo_xl, + FlowySvgs.flowy_logo_xl, size: Size.square(64), blendMode: null, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart index c0b8e7e5ae..8ce09a5b7f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart @@ -8,7 +8,7 @@ class AuthFormContainer extends StatelessWidget { final List children; - static const double width = 320; + static const double width = 340; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart index 14b1c896a9..c2a13eac82 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart @@ -1,7 +1,8 @@ -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; class FlowyLogoTitle extends StatelessWidget { const FlowyLogoTitle({ @@ -15,20 +16,25 @@ class FlowyLogoTitle extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return SizedBox( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - AFLogo(size: logoSize), - const VSpace(20), - Text( - title, - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, + SizedBox.fromSize( + size: logoSize, + child: const FlowySvg( + FlowySvgs.flowy_logo_xl, + blendMode: null, ), ), + const VSpace(20), + FlowyText.regular( + title, + fontSize: FontSizes.s24, + fontFamily: + GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily, + color: Theme.of(context).colorScheme.tertiary, + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/util/default_extensions.dart b/frontend/appflowy_flutter/lib/util/default_extensions.dart index 603a66d6cf..36bbdcb6b4 100644 --- a/frontend/appflowy_flutter/lib/util/default_extensions.dart +++ b/frontend/appflowy_flutter/lib/util/default_extensions.dart @@ -13,11 +13,3 @@ const List defaultImageExtensions = [ 'webp', 'bmp', ]; - -bool isNotImageUrl(String url) { - final nonImageSuffixRegex = RegExp( - r'(\.(io|html|php|json|txt|js|css|xml|md|log)(\?.*)?(#.*)?$)|/$', - caseSensitive: false, - ); - return nonImageSuffixRegex.hasMatch(url); -} diff --git a/frontend/appflowy_flutter/lib/util/share_log_files.dart b/frontend/appflowy_flutter/lib/util/share_log_files.dart index b8dd390627..d7e7b6ce87 100644 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ b/frontend/appflowy_flutter/lib/util/share_log_files.dart @@ -25,6 +25,7 @@ Future shareLogFiles(BuildContext? context) async { if (archiveLogFiles.isEmpty) { if (context != null && context.mounted) { showToastNotification( + context, message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -41,6 +42,7 @@ Future shareLogFiles(BuildContext? context) async { if (zip == null) { if (context != null && context.mounted) { showToastNotification( + context, message: LocaleKeys.noLogFiles.tr(), type: ToastificationType.error, ); @@ -70,6 +72,7 @@ Future shareLogFiles(BuildContext? context) async { } catch (e) { if (context != null && context.mounted) { showToastNotification( + context, message: e.toString(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/util/throttle.dart b/frontend/appflowy_flutter/lib/util/throttle.dart index 0aaa9f2d3a..c8c6dcf0ca 100644 --- a/frontend/appflowy_flutter/lib/util/throttle.dart +++ b/frontend/appflowy_flutter/lib/util/throttle.dart @@ -16,10 +16,6 @@ class Throttler { }); } - void cancel() { - _timer?.cancel(); - } - void dispose() { _timer?.cancel(); _timer = null; diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index 01f638fe7a..d952c09221 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:appflowy/plugins/trash/application/trash_listener.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/workspace/application/command_palette/search_listener.dart'; import 'package:appflowy/workspace/application/command_palette/search_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; @@ -11,338 +13,184 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'command_palette_bloc.freezed.dart'; -class Debouncer { - Debouncer({required this.delay}); - - final Duration delay; - Timer? _timer; - - void run(void Function() action) { - _timer?.cancel(); - _timer = Timer(delay, action); - } - - void dispose() { - _timer?.cancel(); - } -} +const _searchChannel = 'CommandPalette'; class CommandPaletteBloc extends Bloc { CommandPaletteBloc() : super(CommandPaletteState.initial()) { - on<_SearchChanged>(_onSearchChanged); - on<_PerformSearch>(_onPerformSearch); - on<_NewSearchStream>(_onNewSearchStream); - on<_ResultsChanged>(_onResultsChanged); - on<_TrashChanged>(_onTrashChanged); - on<_WorkspaceChanged>(_onWorkspaceChanged); - on<_ClearSearch>(_onClearSearch); + _searchListener.start( + onResultsChanged: _onResultsChanged, + ); _initTrash(); + _dispatch(); } - final Debouncer _searchDebouncer = Debouncer( - delay: const Duration(milliseconds: 300), - ); + Timer? _debounceOnChanged; final TrashService _trashService = TrashService(); + final SearchListener _searchListener = SearchListener( + channel: _searchChannel, + ); final TrashListener _trashListener = TrashListener(); - String? _activeQuery; + String? _oldQuery; String? _workspaceId; + int _messagesReceived = 0; @override Future close() { _trashListener.close(); - _searchDebouncer.dispose(); - state.searchResponseStream?.dispose(); + _searchListener.stop(); + _debounceOnChanged?.cancel(); return super.close(); } + void _dispatch() { + on((event, emit) async { + event.when( + searchChanged: _debounceOnSearchChanged, + trashChanged: (trash) async { + if (trash != null) { + return emit(state.copyWith(trash: trash)); + } + + final trashOrFailure = await _trashService.readTrash(); + final trashRes = trashOrFailure.fold( + (trash) => trash, + (error) => null, + ); + + if (trashRes != null) { + emit(state.copyWith(trash: trashRes.items)); + } + }, + performSearch: (search) async { + if (search.isNotEmpty && search != state.query) { + _oldQuery = state.query; + emit(state.copyWith(query: search, isLoading: true)); + await SearchBackendService.performSearch( + search, + workspaceId: _workspaceId, + channel: _searchChannel, + ); + } else { + emit(state.copyWith(query: null, isLoading: false, results: [])); + } + }, + resultsChanged: (results) { + if (state.query != _oldQuery) { + emit(state.copyWith(results: [], isLoading: true)); + _oldQuery = state.query; + _messagesReceived = 0; + } + + if (state.query != results.query) { + return; + } + + _messagesReceived++; + + emit( + state.copyWith( + results: _filterDuplicates(results.items), + isLoading: _messagesReceived != results.sends.toInt(), + ), + ); + }, + workspaceChanged: (workspaceId) { + _workspaceId = workspaceId; + emit(state.copyWith(results: [], query: '', isLoading: false)); + }, + clearSearch: () { + emit(state.copyWith(results: [], query: '', isLoading: false)); + }, + ); + }); + } + Future _initTrash() async { _trashListener.start( - trashUpdated: (trashOrFailed) => add( - CommandPaletteEvent.trashChanged( - trash: trashOrFailed.toNullable(), - ), - ), + trashUpdated: (trashOrFailed) { + final trash = trashOrFailed.toNullable(); + add(CommandPaletteEvent.trashChanged(trash: trash)); + }, ); final trashOrFailure = await _trashService.readTrash(); - trashOrFailure.fold( - (trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)), - (error) => debugPrint('Failed to load trash: $error'), + final trash = trashOrFailure.toNullable(); + + add(CommandPaletteEvent.trashChanged(trash: trash?.items)); + } + + void _debounceOnSearchChanged(String value) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer( + const Duration(milliseconds: 300), + () => _performSearch(value), ); } - FutureOr _onSearchChanged( - _SearchChanged event, - Emitter emit, - ) { - _searchDebouncer.run( - () { - if (!isClosed) { - add(CommandPaletteEvent.performSearch(search: event.search)); - } - }, - ); - } + List _filterDuplicates(List results) { + final currentItems = [...state.results]; + final res = [...results]; - FutureOr _onPerformSearch( - _PerformSearch event, - Emitter emit, - ) async { - if (event.search.isEmpty && event.search != state.query) { - emit( - state.copyWith( - query: null, - searching: false, - serverResponseItems: [], - localResponseItems: [], - combinedResponseItems: {}, - resultSummaries: [], - generatingAIOverview: false, - ), - ); - } else { - emit(state.copyWith(query: event.search, searching: true)); - _activeQuery = event.search; + for (final item in results) { + final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id); + if (duplicateIndex == -1) { + continue; + } - unawaited( - SearchBackendService.performSearch( - event.search, - workspaceId: _workspaceId, - ).then( - (result) => result.fold( - (stream) { - if (!isClosed && _activeQuery == event.search) { - add(CommandPaletteEvent.newSearchStream(stream: stream)); - } - }, - (error) { - debugPrint('Search error: $error'); - if (!isClosed) { - add( - CommandPaletteEvent.resultsChanged( - searchId: '', - searching: false, - generatingAIOverview: false, - ), - ); - } - }, - ), - ), - ); - } - } - - FutureOr _onNewSearchStream( - _NewSearchStream event, - Emitter emit, - ) { - state.searchResponseStream?.dispose(); - emit( - state.copyWith( - searchId: event.stream.searchId, - searchResponseStream: event.stream, - ), - ); - - event.stream.listen( - onLocalItems: (items, searchId) => _handleResultsUpdate( - searchId: searchId, - localItems: items, - ), - onServerItems: (items, searchId, searching, generatingAIOverview) => - _handleResultsUpdate( - searchId: searchId, - serverItems: items, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - onSummaries: (summaries, searchId, searching, generatingAIOverview) => - _handleResultsUpdate( - searchId: searchId, - summaries: summaries, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - onFinished: (searchId) => _handleResultsUpdate( - searchId: searchId, - searching: false, - ), - ); - } - - void _handleResultsUpdate({ - required String searchId, - List? serverItems, - List? localItems, - List? summaries, - bool searching = true, - bool generatingAIOverview = false, - }) { - if (_isActiveSearch(searchId)) { - add( - CommandPaletteEvent.resultsChanged( - searchId: searchId, - serverItems: serverItems, - localItems: localItems, - summaries: summaries, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - ); - } - } - - FutureOr _onResultsChanged( - _ResultsChanged event, - Emitter emit, - ) async { - if (state.searchId != event.searchId) return; - - final combinedItems = {}; - for (final item in event.serverItems ?? state.serverResponseItems) { - combinedItems[item.id] = SearchResultItem( - id: item.id, - icon: item.icon, - displayName: item.displayName, - content: item.content, - workspaceId: item.workspaceId, - ); + final duplicate = currentItems[duplicateIndex]; + if (item.score < duplicate.score) { + res.remove(item); + } else { + currentItems.remove(duplicate); + } } - for (final item in event.localItems ?? state.localResponseItems) { - combinedItems.putIfAbsent( - item.id, - () => SearchResultItem( - id: item.id, - icon: item.icon, - displayName: item.displayName, - content: '', - workspaceId: item.workspaceId, - ), - ); - } - - emit( - state.copyWith( - serverResponseItems: event.serverItems ?? state.serverResponseItems, - localResponseItems: event.localItems ?? state.localResponseItems, - resultSummaries: event.summaries ?? state.resultSummaries, - combinedResponseItems: combinedItems, - searching: event.searching, - generatingAIOverview: event.generatingAIOverview, - ), - ); + return res..addAll(currentItems); } - FutureOr _onTrashChanged( - _TrashChanged event, - Emitter emit, - ) async { - if (event.trash != null) { - emit(state.copyWith(trash: event.trash!)); - } else { - final trashOrFailure = await _trashService.readTrash(); - trashOrFailure.fold((trash) { - emit(state.copyWith(trash: trash.items)); - }, (error) { - // Optionally handle error; otherwise, we simply do nothing. - }); - } - } + void _performSearch(String value) => + add(CommandPaletteEvent.performSearch(search: value)); - FutureOr _onWorkspaceChanged( - _WorkspaceChanged event, - Emitter emit, - ) { - _workspaceId = event.workspaceId; - emit( - state.copyWith( - query: '', - serverResponseItems: [], - localResponseItems: [], - combinedResponseItems: {}, - resultSummaries: [], - searching: false, - generatingAIOverview: false, - ), - ); - } - - FutureOr _onClearSearch( - _ClearSearch event, - Emitter emit, - ) { - emit(CommandPaletteState.initial().copyWith(trash: state.trash)); - } - - bool _isActiveSearch(String searchId) => - !isClosed && state.searchId == searchId; + void _onResultsChanged(SearchResultNotificationPB results) => + add(CommandPaletteEvent.resultsChanged(results: results)); } @freezed class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.searchChanged({required String search}) = _SearchChanged; + const factory CommandPaletteEvent.performSearch({required String search}) = _PerformSearch; - const factory CommandPaletteEvent.newSearchStream({ - required SearchResponseStream stream, - }) = _NewSearchStream; + const factory CommandPaletteEvent.resultsChanged({ - required String searchId, - required bool searching, - required bool generatingAIOverview, - List? serverItems, - List? localItems, - List? summaries, + required SearchResultNotificationPB results, }) = _ResultsChanged; const factory CommandPaletteEvent.trashChanged({ @Default(null) List? trash, }) = _TrashChanged; + const factory CommandPaletteEvent.workspaceChanged({ @Default(null) String? workspaceId, }) = _WorkspaceChanged; + const factory CommandPaletteEvent.clearSearch() = _ClearSearch; } -class SearchResultItem { - const SearchResultItem({ - required this.id, - required this.icon, - required this.content, - required this.displayName, - this.workspaceId, - }); - - final String id; - final String content; - final ResultIconPB icon; - final String displayName; - final String? workspaceId; -} - @freezed class CommandPaletteState with _$CommandPaletteState { const CommandPaletteState._(); + const factory CommandPaletteState({ @Default(null) String? query, - @Default([]) List serverResponseItems, - @Default([]) List localResponseItems, - @Default({}) Map combinedResponseItems, - @Default([]) List resultSummaries, - @Default(null) SearchResponseStream? searchResponseStream, - required bool searching, - required bool generatingAIOverview, + required List results, + required bool isLoading, @Default([]) List trash, - @Default(null) String? searchId, }) = _CommandPaletteState; - factory CommandPaletteState.initial() => const CommandPaletteState( - searching: false, - generatingAIOverview: false, - ); + factory CommandPaletteState.initial() => + const CommandPaletteState(results: [], isLoading: false); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart new file mode 100644 index 0000000000..b22630eb74 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/search_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; + +// Do not modify! +const _searchObjectId = "SEARCH_IDENTIFIER"; + +class SearchListener { + SearchListener({this.channel}); + + /// Use this to filter out search results from other channels. + /// + /// If null, it will receive search results from all + /// channels, otherwise it will only receive search results from the specified + /// channel. + /// + final String? channel; + + PublishNotifier? _updateNotifier = + PublishNotifier(); + PublishNotifier? _updateDidCloseNotifier = + PublishNotifier(); + SearchNotificationListener? _listener; + + void start({ + void Function(SearchResultNotificationPB)? onResultsChanged, + void Function(SearchResultNotificationPB)? onResultsClosed, + }) { + if (onResultsChanged != null) { + _updateNotifier?.addPublishListener(onResultsChanged); + } + + if (onResultsClosed != null) { + _updateDidCloseNotifier?.addPublishListener(onResultsClosed); + } + + _listener = SearchNotificationListener( + objectId: _searchObjectId, + handler: _handler, + channel: channel, + ); + } + + void _handler( + SearchNotification ty, + FlowyResult result, + ) { + switch (ty) { + case SearchNotification.DidUpdateResults: + result.fold( + (payload) => _updateNotifier?.value = + SearchResultNotificationPB.fromBuffer(payload), + (err) => Log.error(err), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _updateNotifier?.dispose(); + _updateNotifier = null; + _updateDidCloseNotifier?.dispose(); + _updateDidCloseNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart index 6b6ea6d5c0..610c666667 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -5,19 +5,19 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -extension GetIcon on ResultIconPB { +extension GetIcon on SearchResultPB { Widget? getIcon() { - final iconValue = value, iconType = ty; + final iconValue = icon.value, iconType = icon.ty; if (iconType == ResultIconTypePB.Emoji) { return iconValue.isNotEmpty ? FlowyText.emoji(iconValue, fontSize: 18) : null; - } else if (ty == ResultIconTypePB.Icon) { + } else if (icon.ty == ResultIconTypePB.Icon) { if (_resultIconValueTypes.contains(iconValue)) { - return FlowySvg(getViewSvg(), size: const Size.square(18)); + return FlowySvg(icon.getViewSvg(), size: const Size.square(18)); } return RawEmojiIconWidget( - emoji: EmojiIconData(iconType.toFlowyIconType(), value), + emoji: EmojiIconData(iconType.toFlowyIconType(), icon.value), emojiSize: 18, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart deleted file mode 100644 index e5953ae61b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'search_result_list_bloc.freezed.dart'; - -class SearchResultListBloc - extends Bloc { - SearchResultListBloc() : super(SearchResultListState.initial()) { - // Register event handlers - on<_OnHoverSummary>(_onHoverSummary); - on<_OnHoverResult>(_onHoverResult); - on<_OpenPage>(_onOpenPage); - } - - FutureOr _onHoverSummary( - _OnHoverSummary event, - Emitter emit, - ) { - emit( - state.copyWith( - hoveredSummary: event.summary, - hoveredResult: null, - userHovered: event.userHovered, - openPageId: null, - ), - ); - } - - FutureOr _onHoverResult( - _OnHoverResult event, - Emitter emit, - ) { - emit( - state.copyWith( - hoveredSummary: null, - hoveredResult: event.item, - userHovered: event.userHovered, - openPageId: null, - ), - ); - } - - FutureOr _onOpenPage( - _OpenPage event, - Emitter emit, - ) { - emit(state.copyWith(openPageId: event.pageId)); - } -} - -@freezed -class SearchResultListEvent with _$SearchResultListEvent { - const factory SearchResultListEvent.onHoverSummary({ - required SearchSummaryPB summary, - required bool userHovered, - }) = _OnHoverSummary; - const factory SearchResultListEvent.onHoverResult({ - required SearchResultItem item, - required bool userHovered, - }) = _OnHoverResult; - - const factory SearchResultListEvent.openPage({ - required String pageId, - }) = _OpenPage; -} - -@freezed -class SearchResultListState with _$SearchResultListState { - const SearchResultListState._(); - const factory SearchResultListState({ - @Default(null) SearchSummaryPB? hoveredSummary, - @Default(null) SearchResultItem? hoveredResult, - @Default(null) String? openPageId, - @Default(false) bool userHovered, - }) = _SearchResultListState; - - factory SearchResultListState.initial() => const SearchResultListState(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart index 89e5b604f8..53a229ae66 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -1,131 +1,22 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; -import 'dart:typed_data'; - import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/search_filter.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:nanoid/nanoid.dart'; -import 'package:fixnum/fixnum.dart'; class SearchBackendService { - static Future> performSearch( + static Future> performSearch( String keyword, { String? workspaceId, + String? channel, }) async { - final searchId = nanoid(6); - final stream = SearchResponseStream(searchId: searchId); - final filter = SearchFilterPB(workspaceId: workspaceId); final request = SearchQueryPB( search: keyword, filter: filter, - searchId: searchId, - streamPort: Int64(stream.nativePort), + channel: channel, ); - unawaited(SearchEventSearch(request).send()); - return FlowyResult.success(stream); - } -} - -class SearchResponseStream { - SearchResponseStream({required this.searchId}) { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - (Uint8List data) => _onResultsChanged(data), - ); - } - - final String searchId; - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - void Function( - List items, - String searchId, - bool searching, - bool generatingAIOverview, - )? _onServerItems; - void Function( - List summaries, - String searchId, - bool searching, - bool generatingAIOverview, - )? _onSummaries; - - void Function( - List items, - String searchId, - )? _onLocalItems; - - void Function(String searchId)? _onFinished; - int get nativePort => _port.sendPort.nativePort; - - Future dispose() async { - await _subscription.cancel(); - _port.close(); - } - - void _onResultsChanged(Uint8List data) { - final searchState = SearchStatePB.fromBuffer(data); - - if (searchState.hasResponse()) { - if (searchState.response.hasSearchResult()) { - _onServerItems?.call( - searchState.response.searchResult.items, - searchId, - searchState.response.searching, - searchState.response.generatingAiSummary, - ); - } - if (searchState.response.hasSearchSummary()) { - _onSummaries?.call( - searchState.response.searchSummary.items, - searchId, - searchState.response.searching, - searchState.response.generatingAiSummary, - ); - } - - if (searchState.response.hasLocalSearchResult()) { - _onLocalItems?.call( - searchState.response.localSearchResult.items, - searchId, - ); - } - } else { - _onFinished?.call(searchId); - } - } - - void listen({ - required void Function( - List items, - String searchId, - bool isLoading, - bool generatingAIOverview, - )? onServerItems, - required void Function( - List summaries, - String searchId, - bool isLoading, - bool generatingAIOverview, - )? onSummaries, - required void Function( - List items, - String searchId, - )? onLocalItems, - required void Function(String searchId)? onFinished, - }) { - _onServerItems = onServerItems; - _onSummaries = onSummaries; - _onLocalItems = onLocalItems; - _onFinished = onFinished; + return SearchEventSearch(request).send(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 531e797ff5..1afc253ab7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -3,14 +3,14 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceLatestPB; + show WorkspaceSettingPB; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { - HomeBloc(WorkspaceLatestPB workspaceSetting) + HomeBloc(WorkspaceSettingPB workspaceSetting) : _workspaceListener = FolderListener(), super(HomeState.initial(workspaceSetting)) { _dispatch(workspaceSetting); @@ -24,7 +24,7 @@ class HomeBloc extends Bloc { return super.close(); } - void _dispatch(WorkspaceLatestPB workspaceSetting) { + void _dispatch(WorkspaceSettingPB workspaceSetting) { on( (event, emit) async { await event.map( @@ -36,9 +36,10 @@ class HomeBloc extends Bloc { }); _workspaceListener.start( - onLatestUpdated: (result) { + onSettingUpdated: (result) { result.fold( - (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)), + (setting) => + add(HomeEvent.didReceiveWorkspaceSetting(setting)), (r) => Log.error(r), ); }, @@ -77,7 +78,7 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; const factory HomeEvent.didReceiveWorkspaceSetting( - WorkspaceLatestPB setting, + WorkspaceSettingPB setting, ) = _DidReceiveWorkspaceSetting; } @@ -85,11 +86,11 @@ class HomeEvent with _$HomeEvent { class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, ViewPB? latestView, }) = _HomeState; - factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState( + factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( isLoading: false, workspaceSetting: workspaceSetting, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart index cde67045b9..657f2592d7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart @@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_context.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceLatestPB; + show WorkspaceSettingPB; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +12,7 @@ part 'home_setting_bloc.freezed.dart'; class HomeSettingBloc extends Bloc { HomeSettingBloc( - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, double screenWidthPx, ) : _listener = FolderListener(), @@ -124,7 +124,7 @@ class HomeSettingEvent with _$HomeSettingEvent { _ShowEditPanel; const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel; const factory HomeSettingEvent.didReceiveWorkspaceSetting( - WorkspaceLatestPB setting, + WorkspaceSettingPB setting, ) = _DidReceiveWorkspaceSetting; const factory HomeSettingEvent.collapseMenu() = _CollapseMenu; const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) = @@ -139,7 +139,7 @@ class HomeSettingEvent with _$HomeSettingEvent { class HomeSettingState with _$HomeSettingState { const factory HomeSettingState({ required EditPanelContext? panelContext, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, required bool keepMenuCollapsed, @@ -150,7 +150,7 @@ class HomeSettingState with _$HomeSettingState { }) = _HomeSettingState; factory HomeSettingState.initial( - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsState appearanceSettingsState, double screenWidthPx, ) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart index d6a6a73578..5a20b29c09 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -244,10 +244,7 @@ class SidebarSectionsBloc } void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService( - workspaceId: workspaceId, - userId: userProfile.id, - ); + _workspaceService = WorkspaceService(workspaceId: workspaceId); _listener = WorkspaceSectionsListener( user: userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart index 3f9657c5cf..5418eb2b1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart @@ -1,11 +1,8 @@ import 'package:flutter/foundation.dart'; + import 'package:local_notifier/local_notifier.dart'; -/// The app name used in the local notification. -/// -/// DO NOT Use i18n here, because the i18n plugin is not ready -/// before the local notification is initialized. -const _localNotifierAppName = 'AppFlowy'; +const _appName = "AppFlowy"; /// Manages Local Notifications /// @@ -16,11 +13,7 @@ const _localNotifierAppName = 'AppFlowy'; /// class NotificationService { static Future initialize() async { - await localNotifier.setup( - appName: _localNotifierAppName, - // Don't create a shortcut on Windows, because the setup.exe will create a shortcut - shortcutPolicy: ShortcutPolicy.requireNoCreate, - ); + await localNotifier.setup(appName: _appName); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart new file mode 100644 index 0000000000..8be68e813e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_model_bloc.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:fixnum/fixnum.dart'; +part 'download_model_bloc.freezed.dart'; + +class DownloadModelBloc extends Bloc { + DownloadModelBloc(LLMModelPB model) + : super(DownloadModelState.initial(model)) { + on(_handleEvent); + } + + Future _handleEvent( + DownloadModelEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final downloadStream = DownloadingStream(); + downloadStream.listen( + onModelPercentage: (name, percent) { + if (!isClosed) { + add( + DownloadModelEvent.updatePercent(name, percent), + ); + } + }, + onPluginPercentage: (percent) { + if (!isClosed) { + add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent)); + } + }, + onFinish: () { + add(const DownloadModelEvent.downloadFinish()); + }, + onError: (err) { + Log.error(err); + }, + ); + + final payload = + DownloadLLMPB(progressStream: Int64(downloadStream.nativePort)); + final result = await AIEventDownloadLLMResource(payload).send(); + result.fold((_) { + emit( + state.copyWith( + downloadStream: downloadStream, + loadingState: const ChatLoadingState.finish(), + downloadError: null, + ), + ); + }, (err) { + emit( + state.copyWith( + loadingState: ChatLoadingState.finish(error: err), + ), + ); + }); + }, + updatePercent: (String object, double percent) { + emit(state.copyWith(object: object, percent: percent)); + }, + downloadFinish: () { + emit(state.copyWith(isFinish: true)); + }, + ); + } + + @override + Future close() async { + await state.downloadStream?.dispose(); + return super.close(); + } +} + +@freezed +class DownloadModelEvent with _$DownloadModelEvent { + const factory DownloadModelEvent.started() = _Started; + const factory DownloadModelEvent.updatePercent( + String object, + double percent, + ) = _UpdatePercent; + const factory DownloadModelEvent.downloadFinish() = _DownloadFinish; +} + +@freezed +class DownloadModelState with _$DownloadModelState { + const factory DownloadModelState({ + required LLMModelPB model, + DownloadingStream? downloadStream, + String? downloadError, + @Default("") String object, + @Default(0) double percent, + @Default(false) bool isFinish, + String? bigFileDownloadPrompt, + @Default(ChatLoadingState.loading()) ChatLoadingState loadingState, + }) = _DownloadModelState; + + factory DownloadModelState.initial(LLMModelPB model) { + // bigger than 1 GB then show download big file prompt + String? bigFileDownloadPrompt; + if (model.fileSize > 1 * 1024 * 1024 * 1024) { + bigFileDownloadPrompt = + LocaleKeys.settings_aiPage_keys_downloadBigFilePrompt.tr(); + } + return DownloadModelState( + model: model, + bigFileDownloadPrompt: bigFileDownloadPrompt, + ); + } +} + +class DownloadingStream { + DownloadingStream() { + _port.handler = _controller.add; + } + + final RawReceivePort _port = RawReceivePort(); + StreamSubscription? _sub; + final StreamController _controller = StreamController.broadcast(); + int get nativePort => _port.sendPort.nativePort; + + Future dispose() async { + await _sub?.cancel(); + await _controller.close(); + _port.close(); + } + + void listen({ + void Function(String modelName, double percent)? onModelPercentage, + void Function(double percent)? onPluginPercentage, + void Function(String data)? onError, + void Function()? onFinish, + }) { + _sub = _controller.stream.listen((text) { + if (text.contains(':progress:')) { + final progressIndex = text.indexOf(':progress:'); + final modelName = text.substring(0, progressIndex); + final progressValue = text + .substring(progressIndex + 10); // 10 is the length of ":progress:" + final percent = double.tryParse(progressValue); + if (percent != null) { + onModelPercentage?.call(modelName, percent); + } + } else if (text.startsWith('plugin:progress:')) { + final percent = double.tryParse(text.substring(16)); + if (percent != null) { + onPluginPercentage?.call(percent); + } + } else if (text.startsWith('finish')) { + onFinish?.call(); + } else if (text.startsWith('error:')) { + // substring 6 to remove "error:" + onError?.call(text.substring(6)); + } + }); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart new file mode 100644 index 0000000000..829bd2f62a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/download_offline_ai_app_bloc.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:url_launcher/url_launcher.dart' show launchUrl; +part 'download_offline_ai_app_bloc.freezed.dart'; + +class DownloadOfflineAIBloc + extends Bloc { + DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) { + on(_handleEvent); + } + + Future _handleEvent( + DownloadOfflineAIEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final result = await AIEventGetOfflineAIAppLink().send(); + await result.fold( + (app) async { + await launchUrl(Uri.parse(app.link)); + }, + (err) {}, + ); + }, + ); + } +} + +@freezed +class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent { + const factory DownloadOfflineAIEvent.started() = _Started; +} + +@freezed +class DownloadOfflineAIState with _$DownloadOfflineAIState { + const factory DownloadOfflineAIState() = _DownloadOfflineAIState; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart index 492c19ab73..3c3d20039d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart @@ -1,140 +1,93 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'local_llm_listener.dart'; - part 'local_ai_bloc.freezed.dart'; -class LocalAiPluginBloc extends Bloc { - LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) { - on(_handleEvent); - _startListening(); - _getLocalAiState(); - } - - final listener = LocalAIStateListener(); - - @override - Future close() async { - await listener.stop(); - return super.close(); +class LocalAIToggleBloc extends Bloc { + LocalAIToggleBloc() : super(const LocalAIToggleState()) { + on(_handleEvent); } Future _handleEvent( - LocalAiPluginEvent event, - Emitter emit, + LocalAIToggleEvent event, + Emitter emit, ) async { - if (isClosed) { - return; - } - await event.when( - didReceiveAiState: (aiState) { + started: () async { + final result = await AIEventGetLocalAIState().send(); + _handleResult(emit, result); + }, + toggle: () async { emit( - LocalAiPluginState.ready( - isEnabled: aiState.enabled, - version: aiState.pluginVersion, - runningState: aiState.state, - lackOfResource: - aiState.hasLackOfResource() ? aiState.lackOfResource : null, + state.copyWith( + pageIndicator: const LocalAIToggleStateIndicator.loading(), + ), + ); + unawaited( + AIEventToggleLocalAI().send().then( + (result) { + if (!isClosed) { + add(LocalAIToggleEvent.handleResult(result)); + } + }, ), ); }, - didReceiveLackOfResources: (resources) { - state.maybeMap( - ready: (readyState) { - emit(readyState.copyWith(lackOfResource: resources)); - }, - orElse: () {}, - ); - }, - toggle: () async { - emit(LocalAiPluginState.loading()); - await AIEventToggleLocalAI().send().fold( - (aiState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - } - }, - Log.error, - ); - }, - restart: () async { - emit(LocalAiPluginState.loading()); - await AIEventRestartLocalAI().send(); + handleResult: (result) { + _handleResult(emit, result); }, ); } - void _startListening() { - listener.start( - stateCallback: (pluginState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(pluginState)); - } + void _handleResult( + Emitter emit, + FlowyResult result, + ) { + result.fold( + (localAI) { + emit( + state.copyWith( + pageIndicator: LocalAIToggleStateIndicator.ready(localAI.enabled), + ), + ); }, - resourceCallback: (data) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveLackOfResources(data)); - } + (err) { + emit( + state.copyWith( + pageIndicator: LocalAIToggleStateIndicator.error(err), + ), + ); }, ); } - - void _getLocalAiState() { - AIEventGetLocalAIState().send().fold( - (aiState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - } - }, - Log.error, - ); - } } @freezed -class LocalAiPluginEvent with _$LocalAiPluginEvent { - const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) = - _DidReceiveAiState; - const factory LocalAiPluginEvent.didReceiveLackOfResources( - LackOfAIResourcePB resources, - ) = _DidReceiveLackOfResources; - const factory LocalAiPluginEvent.toggle() = _Toggle; - const factory LocalAiPluginEvent.restart() = _Restart; +class LocalAIToggleEvent with _$LocalAIToggleEvent { + const factory LocalAIToggleEvent.started() = _Started; + const factory LocalAIToggleEvent.toggle() = _Toggle; + const factory LocalAIToggleEvent.handleResult( + FlowyResult result, + ) = _HandleResult; } @freezed -class LocalAiPluginState with _$LocalAiPluginState { - const LocalAiPluginState._(); - - const factory LocalAiPluginState.ready({ - required bool isEnabled, - required String version, - required RunningStatePB runningState, - required LackOfAIResourcePB? lackOfResource, - }) = ReadyLocalAiPluginState; - - const factory LocalAiPluginState.loading() = LoadingLocalAiPluginState; - - bool get isEnabled { - return maybeWhen( - ready: (isEnabled, _, __, ___) => isEnabled, - orElse: () => false, - ); - } - - bool get showIndicator { - return maybeWhen( - ready: (isEnabled, _, runningState, lackOfResource) => - runningState != RunningStatePB.Running || lackOfResource != null, - orElse: () => false, - ); - } +class LocalAIToggleState with _$LocalAIToggleState { + const factory LocalAIToggleState({ + @Default(LocalAIToggleStateIndicator.loading()) + LocalAIToggleStateIndicator pageIndicator, + }) = _LocalAIToggleState; +} + +@freezed +class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator { + // when start downloading the model + const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError; + const factory LocalAIToggleStateIndicator.ready(bool isEnabled) = _Ready; + const factory LocalAIToggleStateIndicator.loading() = _Loading; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart new file mode 100644 index 0000000000..7f1df258ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_bloc.dart @@ -0,0 +1,287 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'local_ai_chat_bloc.freezed.dart'; + +class LocalAIChatSettingBloc + extends Bloc { + LocalAIChatSettingBloc() + : listener = LocalLLMListener(), + super(const LocalAIChatSettingState()) { + listener.start( + stateCallback: (newState) { + if (!isClosed) { + add(LocalAIChatSettingEvent.updatePluginState(newState)); + } + }, + ); + + on(_handleEvent); + } + + final LocalLLMListener listener; + + /// Handles incoming events and dispatches them to the appropriate handler. + Future _handleEvent( + LocalAIChatSettingEvent event, + Emitter emit, + ) async { + await event.when( + refreshAISetting: _handleStarted, + didLoadModelInfo: (FlowyResult result) { + result.fold( + (modelInfo) { + _fetchCurremtLLMState(); + emit( + state.copyWith( + modelInfo: modelInfo, + models: modelInfo.models, + selectedLLMModel: modelInfo.selectedModel, + aiModelProgress: const AIModelProgress.finish(), + ), + ); + }, + (err) { + emit( + state.copyWith( + aiModelProgress: AIModelProgress.finish(error: err), + ), + ); + }, + ); + }, + selectLLMConfig: (LLMModelPB llmModel) async { + final result = await AIEventUpdateLocalLLM(llmModel).send(); + result.fold( + (llmResource) { + // If all resources are downloaded, show reload plugin + if (llmResource.pendingResources.isNotEmpty) { + emit( + state.copyWith( + selectedLLMModel: llmModel, + progressIndicator: LocalAIProgress.showDownload( + llmResource, + llmModel, + ), + selectLLMState: const ChatLoadingState.finish(), + ), + ); + } else { + emit( + state.copyWith( + selectedLLMModel: llmModel, + selectLLMState: const ChatLoadingState.finish(), + progressIndicator: const LocalAIProgress.checkPluginState(), + ), + ); + } + }, + (err) { + emit( + state.copyWith( + selectLLMState: ChatLoadingState.finish(error: err), + ), + ); + }, + ); + }, + refreshLLMState: (LocalModelResourcePB llmResource) { + if (state.selectedLLMModel == null) { + Log.error( + 'Unexpected null selected config. It should be set already', + ); + return; + } + + // reload plugin if all resources are downloaded + if (llmResource.pendingResources.isEmpty) { + emit( + state.copyWith( + progressIndicator: const LocalAIProgress.checkPluginState(), + ), + ); + } else { + if (state.selectedLLMModel != null) { + // Go to download page if the selected model is downloading + if (llmResource.isDownloading) { + emit( + state.copyWith( + progressIndicator: + LocalAIProgress.startDownloading(state.selectedLLMModel!), + selectLLMState: const ChatLoadingState.finish(), + ), + ); + return; + } else { + emit( + state.copyWith( + progressIndicator: LocalAIProgress.showDownload( + llmResource, + state.selectedLLMModel!, + ), + selectLLMState: const ChatLoadingState.finish(), + ), + ); + } + } + } + }, + startDownloadModel: (LLMModelPB llmModel) { + emit( + state.copyWith( + progressIndicator: LocalAIProgress.startDownloading(llmModel), + selectLLMState: const ChatLoadingState.finish(), + ), + ); + }, + cancelDownload: () async { + final _ = await AIEventCancelDownloadLLMResource().send(); + _fetchCurremtLLMState(); + }, + finishDownload: () async { + emit( + state.copyWith( + progressIndicator: const LocalAIProgress.finishDownload(), + ), + ); + }, + updatePluginState: (LocalAIPluginStatePB pluginState) { + if (pluginState.offlineAiReady) { + AIEventRefreshLocalAIModelInfo().send().then((result) { + if (!isClosed) { + add(LocalAIChatSettingEvent.didLoadModelInfo(result)); + } + }); + + if (pluginState.state == RunningStatePB.Stopped) { + emit( + state.copyWith( + runningState: pluginState.state, + progressIndicator: const LocalAIProgress.checkPluginState(), + ), + ); + } else { + emit( + state.copyWith( + runningState: pluginState.state, + ), + ); + } + } else { + emit( + state.copyWith( + progressIndicator: const LocalAIProgress.startOfflineAIApp(), + ), + ); + } + }, + ); + } + + void _fetchCurremtLLMState() async { + final result = await AIEventGetLocalLLMState().send(); + result.fold( + (llmResource) { + if (!isClosed) { + add(LocalAIChatSettingEvent.refreshLLMState(llmResource)); + } + }, + (err) { + Log.error(err); + }, + ); + } + + /// Handles the event to fetch local AI settings when the application starts. + Future _handleStarted() async { + final result = await AIEventGetLocalAIPluginState().send(); + result.fold( + (pluginState) async { + if (!isClosed) { + add(LocalAIChatSettingEvent.updatePluginState(pluginState)); + if (pluginState.offlineAiReady) { + final result = await AIEventRefreshLocalAIModelInfo().send(); + if (!isClosed) { + add(LocalAIChatSettingEvent.didLoadModelInfo(result)); + } + } + } + }, + (err) => Log.error(err.toString()), + ); + } + + @override + Future close() async { + await listener.stop(); + return super.close(); + } +} + +@freezed +class LocalAIChatSettingEvent with _$LocalAIChatSettingEvent { + const factory LocalAIChatSettingEvent.refreshAISetting() = _RefreshAISetting; + const factory LocalAIChatSettingEvent.didLoadModelInfo( + FlowyResult result, + ) = _ModelInfo; + const factory LocalAIChatSettingEvent.selectLLMConfig(LLMModelPB config) = + _SelectLLMConfig; + + const factory LocalAIChatSettingEvent.refreshLLMState( + LocalModelResourcePB llmResource, + ) = _RefreshLLMResource; + const factory LocalAIChatSettingEvent.startDownloadModel( + LLMModelPB llmModel, + ) = _StartDownloadModel; + + const factory LocalAIChatSettingEvent.cancelDownload() = _CancelDownload; + const factory LocalAIChatSettingEvent.finishDownload() = _FinishDownload; + const factory LocalAIChatSettingEvent.updatePluginState( + LocalAIPluginStatePB pluginState, + ) = _PluginState; +} + +@freezed +class LocalAIChatSettingState with _$LocalAIChatSettingState { + const factory LocalAIChatSettingState({ + LLMModelInfoPB? modelInfo, + LLMModelPB? selectedLLMModel, + LocalAIProgress? progressIndicator, + @Default(AIModelProgress.init()) AIModelProgress aiModelProgress, + @Default(ChatLoadingState.loading()) ChatLoadingState selectLLMState, + @Default([]) List models, + @Default(RunningStatePB.Connecting) RunningStatePB runningState, + }) = _LocalAIChatSettingState; +} + +@freezed +class LocalAIProgress with _$LocalAIProgress { + // when user comes back to the setting page, it will auto detect current llm state + const factory LocalAIProgress.showDownload( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) = _DownloadNeeded; + + // when start downloading the model + const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) = + _Downloading; + const factory LocalAIProgress.finishDownload() = _Finish; + const factory LocalAIProgress.checkPluginState() = _CheckPluginState; + const factory LocalAIProgress.startOfflineAIApp() = _StartOfflineAIApp; +} + +@freezed +class AIModelProgress with _$AIModelProgress { + const factory AIModelProgress.init() = _AIModelProgressInit; + const factory AIModelProgress.loading() = _AIModelDownloading; + const factory AIModelProgress.finish({FlowyError? error}) = _AIModelFinish; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart new file mode 100644 index 0000000000..4feac1247a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'local_ai_chat_toggle_bloc.freezed.dart'; + +class LocalAIChatToggleBloc + extends Bloc { + LocalAIChatToggleBloc() : super(const LocalAIChatToggleState()) { + on(_handleEvent); + } + + Future _handleEvent( + LocalAIChatToggleEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final result = await AIEventGetLocalAIChatState().send(); + _handleResult(emit, result); + }, + toggle: () async { + emit( + state.copyWith( + pageIndicator: const LocalAIChatToggleStateIndicator.loading(), + ), + ); + unawaited( + AIEventToggleLocalAIChat().send().then( + (result) { + if (!isClosed) { + add(LocalAIChatToggleEvent.handleResult(result)); + } + }, + ), + ); + }, + handleResult: (result) { + _handleResult(emit, result); + }, + ); + } + + void _handleResult( + Emitter emit, + FlowyResult result, + ) { + result.fold( + (localAI) { + emit( + state.copyWith( + pageIndicator: + LocalAIChatToggleStateIndicator.ready(localAI.enabled), + ), + ); + }, + (err) { + emit( + state.copyWith( + pageIndicator: LocalAIChatToggleStateIndicator.error(err), + ), + ); + }, + ); + } +} + +@freezed +class LocalAIChatToggleEvent with _$LocalAIChatToggleEvent { + const factory LocalAIChatToggleEvent.started() = _Started; + const factory LocalAIChatToggleEvent.toggle() = _Toggle; + const factory LocalAIChatToggleEvent.handleResult( + FlowyResult result, + ) = _HandleResult; +} + +@freezed +class LocalAIChatToggleState with _$LocalAIChatToggleState { + const factory LocalAIChatToggleState({ + @Default(LocalAIChatToggleStateIndicator.loading()) + LocalAIChatToggleStateIndicator pageIndicator, + }) = _LocalAIChatToggleState; +} + +@freezed +class LocalAIChatToggleStateIndicator with _$LocalAIChatToggleStateIndicator { + const factory LocalAIChatToggleStateIndicator.error(FlowyError error) = + _OnError; + const factory LocalAIChatToggleStateIndicator.ready(bool isEnabled) = _Ready; + const factory LocalAIChatToggleStateIndicator.loading() = _Loading; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart index 3bb26a182b..2c1bf34a87 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart @@ -26,7 +26,7 @@ class LocalAIOnBoardingBloc _dispatch(); } - void _onPaymentSuccessful() { + Future _onPaymentSuccessful() async { if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart index 99c90faeb5..a7778d7d99 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart @@ -8,11 +8,11 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; -typedef PluginStateCallback = void Function(LocalAIPB state); -typedef PluginResourceCallback = void Function(LackOfAIResourcePB data); +typedef PluginStateCallback = void Function(LocalAIPluginStatePB state); +typedef LocalAIChatCallback = void Function(LocalAIChatPB chatState); -class LocalAIStateListener { - LocalAIStateListener() { +class LocalLLMListener { + LocalLLMListener() { _parser = ChatNotificationParser(id: "appflowy_ai_plugin", callback: _callback); _subscription = RustStreamReceiver.listen( @@ -24,14 +24,15 @@ class LocalAIStateListener { ChatNotificationParser? _parser; PluginStateCallback? stateCallback; - PluginResourceCallback? resourceCallback; + LocalAIChatCallback? chatStateCallback; + void Function()? finishStreamingCallback; void start({ PluginStateCallback? stateCallback, - PluginResourceCallback? resourceCallback, + LocalAIChatCallback? chatStateCallback, }) { this.stateCallback = stateCallback; - this.resourceCallback = resourceCallback; + this.chatStateCallback = chatStateCallback; } void _callback( @@ -40,11 +41,11 @@ class LocalAIStateListener { ) { result.map((r) { switch (ty) { - case ChatNotification.UpdateLocalAIState: - stateCallback?.call(LocalAIPB.fromBuffer(r)); + case ChatNotification.UpdateChatPluginState: + stateCallback?.call(LocalAIPluginStatePB.fromBuffer(r)); break; - case ChatNotification.LocalAIResourceUpdated: - resourceCallback?.call(LackOfAIResourcePB.fromBuffer(r)); + case ChatNotification.UpdateLocalChatAI: + chatStateCallback?.call(LocalAIChatPB.fromBuffer(r)); break; default: break; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart deleted file mode 100644 index f5c4209028..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:equatable/equatable.dart'; - -part 'ollama_setting_bloc.freezed.dart'; - -class OllamaSettingBloc extends Bloc { - OllamaSettingBloc() : super(const OllamaSettingState()) { - on(_handleEvent); - } - - Future _handleEvent( - OllamaSettingEvent event, - Emitter emit, - ) async { - event.when( - started: () { - AIEventGetLocalAISetting().send().fold( - (setting) { - if (!isClosed) { - add(OllamaSettingEvent.didLoadSetting(setting)); - } - }, - Log.error, - ); - }, - didLoadSetting: (setting) => _updateSetting(setting, emit), - updateSetting: (setting) => _updateSetting(setting, emit), - onEdit: (content, settingType) { - final updatedSubmittedItems = state.submittedItems - .map( - (item) => item.settingType == settingType - ? SubmittedItem( - content: content, - settingType: item.settingType, - ) - : item, - ) - .toList(); - - // Convert both lists to maps: {settingType: content} - final updatedMap = { - for (final item in updatedSubmittedItems) - item.settingType: item.content, - }; - - final inputMap = { - for (final item in state.inputItems) item.settingType: item.content, - }; - - // Compare maps instead of lists - final isEdited = !const MapEquality() - .equals(updatedMap, inputMap); - - emit( - state.copyWith( - submittedItems: updatedSubmittedItems, - isEdited: isEdited, - ), - ); - }, - submit: () { - final setting = LocalAISettingPB(); - final settingUpdaters = { - SettingType.serverUrl: (value) => setting.serverUrl = value, - SettingType.chatModel: (value) => setting.chatModelName = value, - SettingType.embeddingModel: (value) => - setting.embeddingModelName = value, - }; - - for (final item in state.submittedItems) { - settingUpdaters[item.settingType]?.call(item.content); - } - add(OllamaSettingEvent.updateSetting(setting)); - AIEventUpdateLocalAISetting(setting).send().fold( - (_) => Log.info('AI setting updated successfully'), - (err) => Log.error("update ai setting failed: $err"), - ); - }, - ); - } - - void _updateSetting( - LocalAISettingPB setting, - Emitter emit, - ) { - emit( - state.copyWith( - setting: setting, - inputItems: _createInputItems(setting), - submittedItems: _createSubmittedItems(setting), - isEdited: false, // Reset to false when the setting is loaded/updated. - ), - ); - } - - List _createInputItems(LocalAISettingPB setting) => [ - SettingItem( - content: setting.serverUrl, - hintText: 'http://localhost:11434', - settingType: SettingType.serverUrl, - ), - SettingItem( - content: setting.chatModelName, - hintText: 'llama3.1', - settingType: SettingType.chatModel, - ), - SettingItem( - content: setting.embeddingModelName, - hintText: 'nomic-embed-text', - settingType: SettingType.embeddingModel, - ), - ]; - - List _createSubmittedItems(LocalAISettingPB setting) => [ - SubmittedItem( - content: setting.serverUrl, - settingType: SettingType.serverUrl, - ), - SubmittedItem( - content: setting.chatModelName, - settingType: SettingType.chatModel, - ), - SubmittedItem( - content: setting.embeddingModelName, - settingType: SettingType.embeddingModel, - ), - ]; -} - -// Create an enum for setting type. -enum SettingType { - serverUrl, - chatModel, - embeddingModel; // semicolon needed after the enum values - - String get title { - switch (this) { - case SettingType.serverUrl: - return 'Ollama server url'; - case SettingType.chatModel: - return 'Chat model name'; - case SettingType.embeddingModel: - return 'Embedding model name'; - } - } -} - -class SettingItem extends Equatable { - const SettingItem({ - required this.content, - required this.hintText, - required this.settingType, - }); - final String content; - final String hintText; - final SettingType settingType; - @override - List get props => [content, settingType]; -} - -class SubmittedItem extends Equatable { - const SubmittedItem({ - required this.content, - required this.settingType, - }); - final String content; - final SettingType settingType; - - @override - List get props => [content, settingType]; -} - -@freezed -class OllamaSettingEvent with _$OllamaSettingEvent { - const factory OllamaSettingEvent.started() = _Started; - const factory OllamaSettingEvent.didLoadSetting(LocalAISettingPB setting) = - _DidLoadSetting; - const factory OllamaSettingEvent.updateSetting(LocalAISettingPB setting) = - _UpdateSetting; - const factory OllamaSettingEvent.onEdit( - String content, - SettingType settingType, - ) = _OnEdit; - const factory OllamaSettingEvent.submit() = _OnSubmit; -} - -@freezed -class OllamaSettingState with _$OllamaSettingState { - const factory OllamaSettingState({ - LocalAISettingPB? setting, - @Default([ - SettingItem( - content: 'http://localhost:11434', - hintText: 'http://localhost:11434', - settingType: SettingType.serverUrl, - ), - SettingItem( - content: 'llama3.1', - hintText: 'llama3.1', - settingType: SettingType.chatModel, - ), - SettingItem( - content: 'nomic-embed-text', - hintText: 'nomic-embed-text', - settingType: SettingType.embeddingModel, - ), - ]) - List inputItems, - @Default([]) List submittedItems, - @Default(false) bool isEdited, - }) = _PluginStateState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart new file mode 100644 index 0000000000..4f24309bde --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/plugin_state_bloc.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:url_launcher/url_launcher.dart' show launchUrl; + +part 'plugin_state_bloc.freezed.dart'; + +class PluginStateBloc extends Bloc { + PluginStateBloc() + : listener = LocalLLMListener(), + super( + const PluginStateState( + action: PluginStateAction.init(), + ), + ) { + listener.start( + stateCallback: (pluginState) { + if (!isClosed) { + add(PluginStateEvent.updateState(pluginState)); + } + }, + ); + + on(_handleEvent); + } + + final LocalLLMListener listener; + + @override + Future close() async { + await listener.stop(); + return super.close(); + } + + Future _handleEvent( + PluginStateEvent event, + Emitter emit, + ) async { + await event.when( + started: () async { + final result = await AIEventGetLocalAIPluginState().send(); + result.fold( + (pluginState) { + if (!isClosed) { + add(PluginStateEvent.updateState(pluginState)); + } + }, + (err) => Log.error(err.toString()), + ); + }, + updateState: (LocalAIPluginStatePB pluginState) { + // if the offline ai is not started, ask user to start it + if (pluginState.offlineAiReady) { + // Chech state of the plugin + switch (pluginState.state) { + case RunningStatePB.Connecting: + emit( + const PluginStateState( + action: PluginStateAction.loadingPlugin(), + ), + ); + case RunningStatePB.Running: + emit(const PluginStateState(action: PluginStateAction.ready())); + break; + default: + emit( + state.copyWith(action: const PluginStateAction.restartPlugin()), + ); + break; + } + } else { + emit( + const PluginStateState( + action: PluginStateAction.startAIOfflineApp(), + ), + ); + } + }, + restartLocalAI: () async { + emit( + const PluginStateState(action: PluginStateAction.loadingPlugin()), + ); + unawaited(AIEventRestartLocalAIChat().send()); + }, + openModelDirectory: () async { + final result = await AIEventGetModelStorageDirectory().send(); + result.fold( + (data) { + afLaunchUri(Uri.file(data.filePath)); + }, + (err) => Log.error(err.toString()), + ); + }, + downloadOfflineAIApp: () async { + final result = await AIEventGetOfflineAIAppLink().send(); + await result.fold( + (app) async { + await launchUrl(Uri.parse(app.link)); + }, + (err) {}, + ); + }, + ); + } +} + +@freezed +class PluginStateEvent with _$PluginStateEvent { + const factory PluginStateEvent.started() = _Started; + const factory PluginStateEvent.updateState(LocalAIPluginStatePB pluginState) = + _UpdatePluginState; + const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI; + const factory PluginStateEvent.openModelDirectory() = + _OpenModelStorageDirectory; + const factory PluginStateEvent.downloadOfflineAIApp() = _DownloadOfflineAIApp; +} + +@freezed +class PluginStateState with _$PluginStateState { + const factory PluginStateState({ + required PluginStateAction action, + }) = _PluginStateState; +} + +@freezed +class PluginStateAction with _$PluginStateAction { + const factory PluginStateAction.init() = _Init; + const factory PluginStateAction.loadingPlugin() = _LoadingPlugin; + const factory PluginStateAction.ready() = _Ready; + const factory PluginStateAction.restartPlugin() = _RestartPlugin; + const factory PluginStateAction.startAIOfflineApp() = _StartAIOfflineApp; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart index 0141283765..ec6970ab7b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart @@ -1,8 +1,9 @@ -import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; +import 'dart:convert'; + import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -10,40 +11,48 @@ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_ai_bloc.freezed.dart'; - -const String aiModelsGlobalActiveModel = "ai_models_global_active_model"; +part 'settings_ai_bloc.g.dart'; class SettingsAIBloc extends Bloc { SettingsAIBloc( this.userProfile, this.workspaceId, + AFRolePB? currentWorkspaceMemberRole, ) : _userListener = UserListener(userProfile: userProfile), - _aiModelSwitchListener = - AIModelSwitchListener(objectId: aiModelsGlobalActiveModel), + _userService = UserBackendService(userId: userProfile.id), super( SettingsAIState( + selectedAIModel: userProfile.aiModel, userProfile: userProfile, + currentWorkspaceMemberRole: currentWorkspaceMemberRole, ), ) { - _aiModelSwitchListener.start( - onUpdateSelectedModel: (model) { - if (!isClosed) { - _loadModelList(); - } - }, - ); _dispatch(); + + if (currentWorkspaceMemberRole == null) { + _userService.getWorkspaceMember().then((result) { + result.fold( + (member) { + if (!isClosed) { + add(SettingsAIEvent.refreshMember(member)); + } + }, + (err) { + Log.error(err); + }, + ); + }); + } } final UserListener _userListener; final UserProfilePB userProfile; + final UserBackendService _userService; final String workspaceId; - final AIModelSwitchListener _aiModelSwitchListener; @override Future close() async { await _userListener.stop(); - await _aiModelSwitchListener.stop(); return super.close(); } @@ -55,12 +64,12 @@ class SettingsAIBloc extends Bloc { onProfileUpdated: _onProfileUpdated, onUserWorkspaceSettingUpdated: (settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); + add(SettingsAIEvent.didLoadAISetting(settings)); } }, ); - _loadModelList(); _loadUserWorkspaceSetting(); + _loadModelList(); }, didReceiveUserProfile: (userProfile) { emit(state.copyWith(userProfile: userProfile)); @@ -74,31 +83,48 @@ class SettingsAIBloc extends Bloc { !(state.aiSettings?.disableSearchIndexing ?? false), ); }, - selectModel: (AIModelPB model) async { - if (!model.isLocal) { - await _updateUserWorkspaceSetting(model: model.name); - } - await AIEventUpdateSelectedModel( - UpdateSelectedModelPB( - source: aiModelsGlobalActiveModel, - selectedModel: model, - ), - ).send(); + selectModel: (String model) async { + await _updateUserWorkspaceSetting(model: model); }, - didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) { + didLoadAISetting: (UseAISettingPB settings) { emit( state.copyWith( aiSettings: settings, + selectedAIModel: settings.aiModel, enableSearchIndexing: !settings.disableSearchIndexing, ), ); }, - didLoadAvailableModels: (AvailableModelsPB models) { - emit( - state.copyWith( - availableModels: models, - ), - ); + didLoadAvailableModels: (String models) { + final dynamic decodedJson = jsonDecode(models); + Log.info("Available models: $decodedJson"); + if (decodedJson is Map) { + final models = ModelList.fromJson(decodedJson).models; + if (models.isEmpty) { + // If available models is empty, then we just show the + // Default + emit(state.copyWith(availableModels: ["Default"])); + return; + } + + if (!models.contains(state.selectedAIModel)) { + // Use first model as default model if current selected model + // is not available + final selectedModel = models[0]; + _updateUserWorkspaceSetting(model: selectedModel); + emit( + state.copyWith( + availableModels: models, + selectedAIModel: selectedModel, + ), + ); + } else { + emit(state.copyWith(availableModels: models)); + } + } + }, + refreshMember: (member) { + emit(state.copyWith(currentWorkspaceMemberRole: member.role)); }, ); }); @@ -133,11 +159,12 @@ class SettingsAIBloc extends Bloc { (err) => Log.error(err), ); - void _loadModelList() { - AIEventGetServerAvailableModels().send().then((result) { - result.fold((models) { + void _loadUserWorkspaceSetting() { + final payload = UserWorkspaceIdPB(workspaceId: workspaceId); + UserEventGetWorkspaceSetting(payload).send().then((result) { + result.fold((settings) { if (!isClosed) { - add(SettingsAIEvent.didLoadAvailableModels(models)); + add(SettingsAIEvent.didLoadAISetting(settings)); } }, (err) { Log.error(err); @@ -145,12 +172,11 @@ class SettingsAIBloc extends Bloc { }); } - void _loadUserWorkspaceSetting() { - final payload = UserWorkspaceIdPB(workspaceId: workspaceId); - UserEventGetWorkspaceSetting(payload).send().then((result) { - result.fold((settings) { + void _loadModelList() { + AIEventGetAvailableModels().send().then((result) { + result.fold((config) { if (!isClosed) { - add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); + add(SettingsAIEvent.didLoadAvailableModels(config.models)); } }, (err) { Log.error(err); @@ -162,20 +188,22 @@ class SettingsAIBloc extends Bloc { @freezed class SettingsAIEvent with _$SettingsAIEvent { const factory SettingsAIEvent.started() = _Started; - const factory SettingsAIEvent.didLoadWorkspaceSetting( - WorkspaceSettingsPB settings, + const factory SettingsAIEvent.didLoadAISetting( + UseAISettingPB settings, ) = _DidLoadWorkspaceSetting; const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; + const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = + _RefreshMember; - const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; + const factory SettingsAIEvent.selectModel(String model) = _SelectAIModel; const factory SettingsAIEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; const factory SettingsAIEvent.didLoadAvailableModels( - AvailableModelsPB models, + String models, ) = _DidLoadAvailableModels; } @@ -183,8 +211,24 @@ class SettingsAIEvent with _$SettingsAIEvent { class SettingsAIState with _$SettingsAIState { const factory SettingsAIState({ required UserProfilePB userProfile, - WorkspaceSettingsPB? aiSettings, - AvailableModelsPB? availableModels, + UseAISettingPB? aiSettings, + @Default("Default") String selectedAIModel, + AFRolePB? currentWorkspaceMemberRole, + @Default(["Default"]) List availableModels, @Default(true) bool enableSearchIndexing, }) = _SettingsAIState; } + +@JsonSerializable() +class ModelList { + ModelList({ + required this.models, + }); + + factory ModelList.fromJson(Map json) => + _$ModelListFromJson(json); + + final List models; + + Map toJson() => _$ModelListToJson(this); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index 99b9eaa2c9..b64ef7d5b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -2,13 +2,11 @@ import 'dart:async'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -19,7 +17,6 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:universal_platform/universal_platform.dart'; part 'appearance_cubit.freezed.dart'; @@ -100,19 +97,7 @@ class AppearanceSettingsCubit extends Cubit { Future setTheme(String themeName) async { _appearanceSettings.theme = themeName; unawaited(_saveAppearanceSettings()); - try { - final theme = await AppTheme.fromName(themeName); - emit(state.copyWith(appTheme: theme)); - } catch (e) { - Log.error("Error setting theme: $e"); - if (UniversalPlatform.isMacOS) { - showToastNotification( - message: - LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(), - type: ToastificationType.error, - ); - } - } + emit(state.copyWith(appTheme: await AppTheme.fromName(themeName))); } /// Reset the current user selected theme back to the default diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index c1e539cf58..e1ea22a6eb 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -14,10 +14,9 @@ class DesktopAppearance extends BaseAppearance { ) { assert(codeFontFamily.isNotEmpty); - fontFamily = fontFamily.isEmpty ? defaultFontFamily : fontFamily; - - final isLight = brightness == Brightness.light; - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; + final theme = brightness == Brightness.light + ? appTheme.lightTheme + : appTheme.darkTheme; final colorScheme = ColorScheme( brightness: brightness, @@ -151,7 +150,6 @@ class DesktopAppearance extends BaseAppearance { scrollbarColor: theme.scrollbarColor, scrollbarHoverColor: theme.scrollbarHoverColor, lightIconColor: theme.lightIconColor, - toolbarHoverColor: theme.toolbarHoverColor, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 46eddd53ab..6aa649d320 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -28,12 +28,13 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, ); - final isLight = brightness == Brightness.light; final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; + final theme = brightness == Brightness.light + ? appTheme.lightTheme + : appTheme.darkTheme; - final colorTheme = isLight + final colorTheme = brightness == Brightness.light ? ColorScheme( brightness: brightness, primary: _primaryColor, @@ -70,9 +71,13 @@ class MobileAppearance extends BaseAppearance { onSurface: const Color(0xffC5C6C7), // text/body color surfaceContainerHighest: theme.sidebarBg, ); - final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode; - final onBackground = isLight ? _onBackgroundColor : Colors.white; - final background = isLight ? Colors.white : const Color(0xff121212); + final hintColor = brightness == Brightness.light + ? const Color(0x991F2329) + : _hintColorInDarkMode; + final onBackground = + brightness == Brightness.light ? _onBackgroundColor : Colors.white; + final background = + brightness == Brightness.light ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, @@ -275,7 +280,6 @@ class MobileAppearance extends BaseAppearance { scrollbarColor: theme.scrollbarColor, scrollbarHoverColor: theme.scrollbarHoverColor, lightIconColor: theme.lightIconColor, - toolbarHoverColor: theme.toolbarHoverColor, ), ToolbarColorExtension.fromBrightness(brightness), ], diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart index df880891e9..ab324df87f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart @@ -29,7 +29,7 @@ class SettingsBillingBloc required Int64 userId, }) : super(const _Initial()) { _userService = UserBackendService(userId: userId); - _service = WorkspaceService(workspaceId: workspaceId, userId: userId); + _service = WorkspaceService(workspaceId: workspaceId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart index 26975b00ff..f7512a834e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart @@ -23,10 +23,7 @@ class SettingsPlanBloc extends Bloc { required this.workspaceId, required Int64 userId, }) : super(const _Initial()) { - _service = WorkspaceService( - workspaceId: workspaceId, - userId: userId, - ); + _service = WorkspaceService(workspaceId: workspaceId); _userService = UserBackendService(userId: userId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); @@ -46,7 +43,7 @@ class SettingsPlanBloc extends Bloc { FlowyError? error; final usageResult = snapshots.first.fold( - (s) => s as WorkspaceUsagePB?, + (s) => s as WorkspaceUsagePB, (f) { error = f; return null; @@ -151,7 +148,7 @@ class SettingsPlanBloc extends Bloc { usage.freeze(); final newUsage = usage.rebuild((value) { - if (!newInfo.hasAIMax) { + if (!newInfo.hasAIMax && !newInfo.hasAIOnDevice) { value.aiResponsesUnlimited = false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 83588f0079..0578d9808b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -90,8 +91,8 @@ class SettingsDialogBloc AFRolePB? currentWorkspaceMemberRole, ]) async { if ([ - AuthTypePB.Local, - ].contains(userProfile.workspaceAuthType)) { + AuthenticatorPB.Local, + ].contains(userProfile.authenticator)) { return false; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart index 56d6ae8cc8..1737494530 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/file_storage/file_storage_listener.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; -import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; @@ -183,24 +182,17 @@ class SidebarPlanBloc extends Bloc { ); } - Future _checkWorkspaceUsage() async { - if (state.workspaceId == null || state.userProfile == null) { - return; - } - - await WorkspaceService( - workspaceId: state.workspaceId!, - userId: state.userProfile!.id, - ).getWorkspaceUsage().then((result) { - result.fold( - (usage) { - if (!isClosed && usage != null) { + void _checkWorkspaceUsage() { + if (state.workspaceId != null) { + final payload = UserWorkspaceIdPB(workspaceId: state.workspaceId!); + UserEventGetWorkspaceUsage(payload).send().then((result) { + result.onSuccess( + (usage) { add(SidebarPlanEvent.updateWorkspaceUsage(usage)); - } - }, - (error) => Log.error("Failed to get workspace usage: $error"), - ); - }); + }, + ); + }); + } } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index 9de0c582cd..0f1dc4e987 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -76,6 +76,12 @@ class SpaceBloc extends Bloc { final (spaces, publicViews, privateViews) = await _getSpaces(); + final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog( + spaces: spaces, + publicViews: publicViews, + privateViews: privateViews, + ); + final currentSpace = await _getLastOpenedSpace(spaces); final isExpanded = await _getSpaceExpandStatus(currentSpace); emit( @@ -83,11 +89,17 @@ class SpaceBloc extends Bloc { spaces: spaces, currentSpace: currentSpace, isExpanded: isExpanded, - shouldShowUpgradeDialog: false, + shouldShowUpgradeDialog: shouldShowUpgradeDialog, isInitialized: true, ), ); + if (shouldShowUpgradeDialog && !integrationMode().isTest) { + if (!isClosed) { + add(const SpaceEvent.migrate()); + } + } + if (openFirstPage) { if (currentSpace != null) { if (!isClosed) { @@ -319,6 +331,16 @@ class SpaceBloc extends Bloc { final (spaces, _, _) = await _getSpaces(); final currentSpace = await _getLastOpenedSpace(spaces); + Log.info( + 'receive space update, current space: ${currentSpace?.name}(${currentSpace?.id})', + ); + + for (var i = 0; i < spaces.length; i++) { + Log.info( + 'receive space update[$i]: ${spaces[i].name}(${spaces[i].id})', + ); + } + emit( state.copyWith( spaces: spaces, @@ -474,10 +496,8 @@ class SpaceBloc extends Bloc { } void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService( - workspaceId: workspaceId, - userId: userProfile.id, - ); + Log.info('initial(or reset) space bloc: $workspaceId, ${userProfile.id}'); + _workspaceService = WorkspaceService(workspaceId: workspaceId); this.userProfile = userProfile; this.workspaceId = workspaceId; @@ -487,6 +507,7 @@ class SpaceBloc extends Bloc { workspaceId: workspaceId, )..start( sectionChanged: (result) async { + Log.info('did receive section views changed'); if (isClosed) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 2f62177661..56faa9f8d8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -54,24 +54,6 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - updateUserEmail: (String email) { - _userService.updateUserProfile(email: email).then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, - updateUserPassword: (String oldPassword, String newPassword) { - _userService - .updateUserProfile(password: newPassword) - .then((result) { - result.fold( - (l) => null, - (err) => Log.error(err), - ); - }); - }, removeUserIcon: () { // Empty Icon URL = No icon _userService.updateUserProfile(iconUrl: "").then((result) { @@ -81,6 +63,14 @@ class SettingsUserViewBloc extends Bloc { ); }); }, + updateUserEmail: (String email) { + _userService.updateUserProfile(email: email).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, ); }, ); @@ -114,19 +104,10 @@ class SettingsUserViewBloc extends Bloc { @freezed class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; - const factory SettingsUserEvent.updateUserName({ - required String name, - }) = _UpdateUserName; - const factory SettingsUserEvent.updateUserEmail({ - required String email, - }) = _UpdateEmail; - const factory SettingsUserEvent.updateUserIcon({ - required String iconUrl, - }) = _UpdateUserIcon; - const factory SettingsUserEvent.updateUserPassword({ - required String oldPassword, - required String newPassword, - }) = _UpdateUserPassword; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail; + const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) = + _UpdateUserIcon; const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index d14f258462..7f32a86d1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -44,7 +44,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.userAuthType == AuthTypePB.Server && + userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' @@ -52,10 +52,7 @@ class UserWorkspaceBloc extends Bloc { ); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); - await _userService.openWorkspace( - currentWorkspace.workspaceId, - currentWorkspace.workspaceAuthType, - ); + await _userService.openWorkspace(currentWorkspace.workspaceId); } emit( @@ -89,15 +86,10 @@ class UserWorkspaceBloc extends Bloc { Log.info( 'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}', ); - add( - OpenWorkspace( - currentWorkspace.workspaceId, - currentWorkspace.workspaceAuthType, - ), - ); + add(OpenWorkspace(currentWorkspace.workspaceId)); } }, - createWorkspace: (name, authType) async { + createWorkspace: (name) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -107,10 +99,7 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.createUserWorkspace( - name, - authType, - ); + final result = await _userService.createUserWorkspace(name); final workspaces = result.fold( (s) => [...state.workspaces, s], (e) => state.workspaces, @@ -129,12 +118,7 @@ class UserWorkspaceBloc extends Bloc { result ..onSuccess((s) { Log.info('create workspace success: $s'); - add( - OpenWorkspace( - s.workspaceId, - s.workspaceAuthType, - ), - ); + add(OpenWorkspace(s.workspaceId)); }) ..onFailure((f) { Log.error('create workspace error: $f'); @@ -187,12 +171,7 @@ class UserWorkspaceBloc extends Bloc { Log.info('delete workspace success: $workspaceId'); // if the current workspace is deleted, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); + add(OpenWorkspace(workspaces.first.workspaceId)); } }) ..onFailure((f) { @@ -200,12 +179,7 @@ class UserWorkspaceBloc extends Bloc { // if the workspace is deleted but return an error, we need to // open the first workspace if (!containsDeletedWorkspace) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); + add(OpenWorkspace(workspaces.first.workspaceId)); } }); emit( @@ -219,7 +193,7 @@ class UserWorkspaceBloc extends Bloc { ), ); }, - openWorkspace: (workspaceId, authType) async { + openWorkspace: (workspaceId) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -229,10 +203,7 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.openWorkspace( - workspaceId, - authType, - ); + final result = await _userService.openWorkspace(workspaceId); final currentWorkspace = result.fold( (s) => state.workspaces.firstWhereOrNull( (e) => e.workspaceId == workspaceId, @@ -366,12 +337,7 @@ class UserWorkspaceBloc extends Bloc { Log.info('leave workspace success: $workspaceId'); // if leaving the current workspace, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); + add(OpenWorkspace(workspaces.first.workspaceId)); } }) ..onFailure((f) { @@ -475,16 +441,12 @@ class UserWorkspaceBloc extends Bloc { class UserWorkspaceEvent with _$UserWorkspaceEvent { const factory UserWorkspaceEvent.initial() = Initial; const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; - const factory UserWorkspaceEvent.createWorkspace( - String name, - AuthTypePB authType, - ) = CreateWorkspace; + const factory UserWorkspaceEvent.createWorkspace(String name) = + CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; - const factory UserWorkspaceEvent.openWorkspace( - String workspaceId, - AuthTypePB authType, - ) = OpenWorkspace; + const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = + OpenWorkspace; const factory UserWorkspaceEvent.renameWorkspace( String workspaceId, String name, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 553317f4e4..7c2a4d9b64 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -404,7 +404,7 @@ class ViewBloc extends Bloc { }); } - if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) { + if (update.updateChildViews.isNotEmpty) { final view = await ViewBackendService.getView(update.parentViewId); final childViews = view.fold((l) => l.childViews, (r) => []); bool isSameOrder = true; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index ea74f1861e..709515f1b3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -111,12 +111,6 @@ class ViewBackendService { static Future, FlowyError>> getChildViews({ required String viewId, }) { - if (viewId.isEmpty) { - return Future.value( - FlowyResult, FlowyError>.success([]), - ); - } - final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send().then((result) { @@ -268,9 +262,6 @@ class ViewBackendService { static Future> getView( String viewId, ) async { - if (viewId.isEmpty) { - Log.error('ViewId is empty'); - } final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index ed06f16c8f..d8d5db45b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -2,7 +2,6 @@ import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -65,8 +64,7 @@ class WorkspaceBloc extends Bloc { String desc, Emitter emit, ) async { - final result = - await userService.createUserWorkspace(name, AuthTypePB.Server); + final result = await userService.createWorkspace(name, desc); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index ae6220994e..b958e5cd30 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -1,18 +1,15 @@ import 'dart:async'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart' as fixnum; class WorkspaceService { - WorkspaceService({required this.workspaceId, required this.userId}); + WorkspaceService({required this.workspaceId}); final String workspaceId; - final fixnum.Int64 userId; Future> createView({ required String name, @@ -85,18 +82,7 @@ class WorkspaceService { return FolderEventMoveView(payload).send(); } - Future> getWorkspaceUsage() async { - final request = WorkspaceMemberIdPB()..uid = userId; - final result = await UserEventGetMemberInfo(request).send(); - final isOwner = result.fold( - (member) => member.role.isOwner, - (_) => false, - ); - - if (!isOwner) { - return FlowyResult.success(null); - } - + Future> getWorkspaceUsage() { final payload = UserWorkspaceIdPB(workspaceId: workspaceId); return UserEventGetWorkspaceUsage(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index 648712bd15..8eb7765c3a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -134,17 +135,13 @@ class CommandPaletteModal extends StatelessWidget { builder: (context, state) => FlowyDialog( alignment: Alignment.topCenter, insetPadding: const EdgeInsets.only(top: 100), - constraints: const BoxConstraints( - maxHeight: 600, - maxWidth: 800, - minHeight: 600, - ), + constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510), expandHeight: false, child: shortcutBuilder( - // Change mainAxisSize to max so Expanded works correctly. Column( + mainAxisSize: MainAxisSize.min, children: [ - SearchField(query: state.query, isLoading: state.searching), + SearchField(query: state.query, isLoading: state.isLoading), if (state.query?.isEmpty ?? true) ...[ const Divider(height: 0), Flexible( @@ -153,26 +150,23 @@ class CommandPaletteModal extends StatelessWidget { ), ), ], - if (state.combinedResponseItems.isNotEmpty && + if (state.results.isNotEmpty && (state.query?.isNotEmpty ?? false)) ...[ const Divider(height: 0), Flexible( - child: SearchResultList( + child: SearchResultsList( trash: state.trash, - resultItems: state.combinedResponseItems.values.toList(), - resultSummaries: state.resultSummaries, + results: state.results, ), ), - ] - // When there are no results and the query is not empty and not loading, - // show the no results message, centered in the available space. - else if ((state.query?.isNotEmpty ?? false) && - !state.searching) ...[ - const Divider(height: 0), - Expanded( - child: const _NoResultsHint(), - ), + ] else if ((state.query?.isNotEmpty ?? false) && + !state.isLoading) ...[ + const _NoResultsHint(), ], + _CommandPaletteFooter( + shouldShow: state.results.isNotEmpty && + (state.query?.isNotEmpty ?? false), + ), ], ), ), @@ -181,16 +175,57 @@ class CommandPaletteModal extends StatelessWidget { } } -/// Updated _NoResultsHint now centers its content. class _NoResultsHint extends StatelessWidget { const _NoResultsHint(); @override Widget build(BuildContext context) { - return Center( - child: FlowyText.regular( - LocaleKeys.commandPalette_noResultsHint.tr(), - textAlign: TextAlign.center, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FlowyText.regular( + LocaleKeys.commandPalette_noResultsHint.tr(), + textAlign: TextAlign.left, + ), + ), + ], + ); + } +} + +class _CommandPaletteFooter extends StatelessWidget { + const _CommandPaletteFooter({required this.shouldShow}); + + final bool shouldShow; + + @override + Widget build(BuildContext context) { + if (!shouldShow) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: const FlowyText.semibold('TAB', fontSize: 10), + ), + const HSpace(4), + FlowyText(LocaleKeys.commandPalette_navigateHint.tr(), fontSize: 11), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart similarity index 94% rename from frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart index a803f9b44c..645b9696c8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -7,8 +7,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -class SearchRecentViewCell extends StatelessWidget { - const SearchRecentViewCell({ +class RecentViewTile extends StatelessWidget { + const RecentViewTile({ super.key, required this.icon, required this.view, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index 3bc160ee81..b0f87005d2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -52,7 +52,7 @@ class RecentViewsList extends StatelessWidget { ) : FlowySvg(view.iconData, size: const Size.square(20)); - return SearchRecentViewCell( + return RecentViewTile( icon: SizedBox(width: 24, child: icon), view: view, onSelected: onSelected, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart index 1586ab0a7e..c18024a909 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -24,31 +25,28 @@ class SearchField extends StatefulWidget { class _SearchFieldState extends State { late final FocusNode focusNode; - late final TextEditingController controller; + late final controller = TextEditingController(text: widget.query); @override void initState() { super.initState(); - controller = TextEditingController(text: widget.query); - focusNode = FocusNode(onKeyEvent: _handleKeyEvent); - focusNode.requestFocus(); - // Update the text selection after the first frame - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.selection = TextSelection( - baseOffset: 0, - extentOffset: controller.text.length, - ); - }); - } + focusNode = FocusNode( + onKeyEvent: (node, event) { + if (node.hasFocus && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + node.nextFocus(); + return KeyEventResult.handled; + } - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (node.hasFocus && - event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.arrowDown) { - node.nextFocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + return KeyEventResult.ignored; + }, + ); + focusNode.requestFocus(); + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); } @override @@ -58,83 +56,21 @@ class _SearchFieldState extends State { super.dispose(); } - Widget _buildSuffixIcon(BuildContext context) { - return ValueListenableBuilder( - valueListenable: controller, - builder: (context, value, _) { - final hasText = value.text.trim().isNotEmpty; - final clearIcon = Container( - padding: const EdgeInsets.all(1), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AFThemeExtension.of(context).lightGreyHover, - ), - child: const FlowySvg( - FlowySvgs.close_s, - size: Size.square(16), - ), - ); - return AnimatedOpacity( - opacity: hasText ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: hasText - ? FlowyTooltip( - message: LocaleKeys.commandPalette_clearSearchTooltip.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: _clearSearch, - child: clearIcon, - ), - ), - ) - : clearIcon, - ); - }, - ); - } - @override Widget build(BuildContext context) { - // Cache theme and text styles - final theme = Theme.of(context); - final textStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 14); - final hintStyle = theme.textTheme.bodySmall?.copyWith( - fontSize: 14, - color: theme.hintColor, - ); - - // Choose the leading icon based on loading state - final Widget leadingIcon = widget.isLoading - ? FlowyTooltip( - message: LocaleKeys.commandPalette_loadingTooltip.tr(), - child: const SizedBox( - width: 20, - height: 20, - child: Padding( - padding: EdgeInsets.all(3.0), - child: CircularProgressIndicator(strokeWidth: 2.0), - ), - ), - ) - : SizedBox( - width: 20, - height: 20, - child: FlowySvg( - FlowySvgs.search_m, - color: theme.hintColor, - ), - ); - return Row( children: [ const HSpace(12), - leadingIcon, + FlowySvg( + FlowySvgs.search_m, + color: Theme.of(context).hintColor, + ), Expanded( child: FlowyTextField( focusNode: focusNode, controller: controller, - textStyle: textStyle, + textStyle: + Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14), decoration: InputDecoration( constraints: const BoxConstraints(maxHeight: 48), contentPadding: const EdgeInsets.symmetric(horizontal: 12), @@ -144,14 +80,72 @@ class _SearchFieldState extends State { ), isDense: false, hintText: LocaleKeys.commandPalette_placeholder.tr(), - hintStyle: hintStyle, - errorStyle: theme.textTheme.bodySmall! - .copyWith(color: theme.colorScheme.error), + hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 14, + color: Theme.of(context).hintColor, + ), + errorStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.error), suffix: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildSuffixIcon(context), + AnimatedOpacity( + opacity: controller.text.trim().isNotEmpty ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Builder( + builder: (context) { + final icon = Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AFThemeExtension.of(context).lightGreyHover, + ), + child: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(16), + ), + ); + if (controller.text.isEmpty) { + return icon; + } + + return FlowyTooltip( + message: + LocaleKeys.commandPalette_clearSearchTooltip.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: controller.text.trim().isNotEmpty + ? _clearSearch + : null, + child: icon, + ), + ), + ); + }, + ), + ), const HSpace(8), + FlowyTooltip( + message: LocaleKeys.commandPalette_betaTooltip.tr(), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText.semibold( + LocaleKeys.commandPalette_betaLabel.tr(), + fontSize: 11, + lineHeight: 1.2, + ), + ), + ), ], ), counterText: "", @@ -161,7 +155,9 @@ class _SearchFieldState extends State { ), errorBorder: OutlineInputBorder( borderRadius: Corners.s8Border, - borderSide: BorderSide(color: theme.colorScheme.error), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), ), ), onChanged: (value) => context @@ -169,6 +165,17 @@ class _SearchFieldState extends State { .add(CommandPaletteEvent.searchChanged(search: value)), ), ), + if (widget.isLoading) ...[ + FlowyTooltip( + message: LocaleKeys.commandPalette_loadingTooltip.tr(), + child: const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2.5), + ), + ), + const HSpace(12), + ], ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart deleted file mode 100644 index 2485da4a69..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ /dev/null @@ -1,235 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SearchResultCell extends StatefulWidget { - const SearchResultCell({ - super.key, - required this.item, - this.isTrashed = false, - this.isHovered = false, - }); - - final SearchResultItem item; - final bool isTrashed; - final bool isHovered; - - @override - State createState() => _SearchResultCellState(); -} - -class _SearchResultCellState extends State { - bool _hasFocus = false; - final focusNode = FocusNode(); - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - /// Helper to handle the selection action. - void _handleSelection() { - context.read().add( - SearchResultListEvent.openPage(pageId: widget.item.id), - ); - } - - /// Helper to clean up preview text. - String _cleanPreview(String preview) { - return preview.replaceAll('\n', ' ').trim(); - } - - @override - Widget build(BuildContext context) { - final title = widget.item.displayName.orDefault( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - ); - final icon = widget.item.icon.getIcon(); - final cleanedPreview = _cleanPreview(widget.item.content); - final hasPreview = cleanedPreview.isNotEmpty; - final trashHintText = - widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null; - - // Build the tile content based on preview availability. - Widget tileContent; - if (hasPreview) { - tileContent = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.isTrashed) - FlowyText( - trashHintText!, - color: AFThemeExtension.of(context) - .textColor - .withAlpha(175), - fontSize: 10, - ), - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - const VSpace(4), - _DocumentPreview(preview: cleanedPreview), - ], - ); - } else { - tileContent = Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.isTrashed) - FlowyText( - trashHintText!, - color: - AFThemeExtension.of(context).textColor.withAlpha(175), - fontSize: 10, - ), - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleSelection, - child: Focus( - focusNode: focusNode, - onKeyEvent: (node, event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.enter) { - _handleSelection(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - onFocusChange: (hasFocus) { - setState(() { - context.read().add( - SearchResultListEvent.onHoverResult( - item: widget.item, - userHovered: true, - ), - ); - _hasFocus = hasFocus; - }); - }, - child: FlowyHover( - onHover: (value) { - context.read().add( - SearchResultListEvent.onHoverResult( - item: widget.item, - userHovered: true, - ), - ); - }, - isSelected: () => _hasFocus || widget.isHovered, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: tileContent, - ), - ), - ), - ), - ); - } -} - -class _DocumentPreview extends StatelessWidget { - const _DocumentPreview({required this.preview}); - - final String preview; - - @override - Widget build(BuildContext context) { - // Combine the horizontal padding for clarity: - return Padding( - padding: const EdgeInsets.fromLTRB(30, 0, 16, 0), - child: FlowyText.regular( - preview, - color: Theme.of(context).hintColor, - fontSize: 12, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ); - } -} - -class SearchResultPreview extends StatelessWidget { - const SearchResultPreview({ - super.key, - required this.data, - }); - - final SearchResultItem data; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_pagePreview.tr(), - fontSize: 12, - ), - ), - const VSpace(6), - Expanded( - child: FlowyText( - data.content, - maxLines: 30, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart new file mode 100644 index 0000000000..5f26f07597 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -0,0 +1,162 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SearchResultTile extends StatefulWidget { + const SearchResultTile({ + super.key, + required this.result, + required this.onSelected, + this.isTrashed = false, + }); + + final SearchResultPB result; + final VoidCallback onSelected; + final bool isTrashed; + + @override + State createState() => _SearchResultTileState(); +} + +class _SearchResultTileState extends State { + bool _hasFocus = false; + + final focusNode = FocusNode(); + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final title = widget.result.data.orDefault( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ); + final icon = widget.result.getIcon(); + final cleanedPreview = _cleanPreview(widget.result.preview); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.result.viewId), + ), + ); + }, + child: Focus( + onKeyEvent: (node, event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.enter) { + widget.onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.result.viewId), + ), + ); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), + child: FlowyHover( + isSelected: () => _hasFocus, + style: HoverStyle( + hoverColor: + Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // page icon + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // if the result is trashed, show a hint + if (widget.isTrashed) ...[ + FlowyText( + LocaleKeys.commandPalette_fromTrashHint.tr(), + color: AFThemeExtension.of(context) + .textColor + .withAlpha(175), + fontSize: 10, + ), + ], + // page title + FlowyText( + title, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + // content preview + if (cleanedPreview.isNotEmpty) ...[ + const VSpace(4), + _DocumentPreview(preview: cleanedPreview), + ], + ], + ), + ), + ), + ), + ); + } + + String _cleanPreview(String preview) { + return preview.replaceAll('\n', ' ').trim(); + } +} + +class _DocumentPreview extends StatelessWidget { + const _DocumentPreview({required this.preview}); + + final String preview; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16) + + const EdgeInsets.only(left: 14), + child: FlowyText.regular( + preview, + color: Theme.of(context).hintColor, + fontSize: 12, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index d90888e3e9..ed9becf29e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -1,278 +1,47 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'search_result_cell.dart'; -import 'search_summary_cell.dart'; - -class SearchResultList extends StatefulWidget { - const SearchResultList({ - required this.trash, - required this.resultItems, - required this.resultSummaries, +class SearchResultsList extends StatelessWidget { + const SearchResultsList({ super.key, + required this.trash, + required this.results, }); final List trash; - final List resultItems; - final List resultSummaries; + final List results; @override - State createState() => _SearchResultListState(); -} - -class _SearchResultListState extends State { - late final SearchResultListBloc bloc; - - @override - void initState() { - super.initState(); - bloc = SearchResultListBloc(); - } - - @override - void dispose() { - bloc.close(); - super.dispose(); - } - - Widget _buildSectionHeader(String title) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8) + - const EdgeInsets.only(left: 8), - child: Opacity( - opacity: 0.6, - child: FlowyText(title, fontSize: 12), - ), - ); - - Widget _buildAIOverviewSection(BuildContext context) { - final state = context.read().state; - - if (state.generatingAIOverview) { - return Row( - children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), - const HSpace(10), - const AIOverviewIndicator(), - ], - ); - } - - if (widget.resultSummaries.isNotEmpty) { - if (!bloc.state.userHovered) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - bloc.add( - SearchResultListEvent.onHoverSummary( - summary: widget.resultSummaries[0], - userHovered: false, - ), - ); - }, - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.resultSummaries.length, - separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) => SearchSummaryCell( - summary: widget.resultSummaries[index], - isHovered: bloc.state.hoveredSummary != null, + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(height: 0), + itemCount: results.length + 1, + itemBuilder: (_, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 16), + child: FlowyText( + LocaleKeys.commandPalette_bestMatches.tr(), ), - ), - ], - ); - } - - return const SizedBox.shrink(); - } - - Widget _buildResultsSection(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _buildSectionHeader(LocaleKeys.commandPalette_bestMatches.tr()), - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.resultItems.length, - separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) { - final item = widget.resultItems[index]; - return SearchResultCell( - item: item, - isTrashed: widget.trash.any((t) => t.id == item.id), - isHovered: bloc.state.hoveredResult?.id == item.id, - ); - }, - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: BlocProvider.value( - value: bloc, - child: BlocListener( - listener: (context, state) { - if (state.openPageId != null) { - FlowyOverlay.pop(context); - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: state.openPageId!), - ), - ); - } - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - flex: 7, - child: BlocBuilder( - buildWhen: (previous, current) => - previous.hoveredResult != current.hoveredResult || - previous.hoveredSummary != current.hoveredSummary, - builder: (context, state) { - return ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - _buildAIOverviewSection(context), - const VSpace(10), - if (widget.resultItems.isNotEmpty) - _buildResultsSection(context), - ], - ); - }, - ), - ), - const HSpace(10), - if (widget.resultItems - .any((item) => item.content.isNotEmpty)) ...[ - const VerticalDivider( - thickness: 1.0, - ), - Flexible( - flex: 3, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - child: const SearchCellPreview(), - ), - ), - ], - ], - ), - ), - ), - ); - } -} - -class SearchCellPreview extends StatelessWidget { - const SearchCellPreview({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.hoveredSummary != null) { - return SearchSummaryPreview(summary: state.hoveredSummary!); - } else if (state.hoveredResult != null) { - return SearchResultPreview(data: state.hoveredResult!); + ); } - return const SizedBox.shrink(); + + final result = results[index - 1]; + return SearchResultTile( + result: result, + onSelected: () => FlowyOverlay.pop(context), + isTrashed: trash.any((t) => t.id == result.viewId), + ); }, ); } } - -class AIOverviewIndicator extends StatelessWidget { - const AIOverviewIndicator({ - super.key, - this.duration = const Duration(seconds: 1), - }); - - final Duration duration; - - @override - Widget build(BuildContext context) { - final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return SelectionContainer.disabled( - child: SizedBox( - height: 20, - width: 100, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], - ), - ), - ); - } - - Widget buildDot(Color color) { - return SizedBox.square( - dimension: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart deleted file mode 100644 index 84b8f6646b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SearchSummaryCell extends StatelessWidget { - const SearchSummaryCell({ - required this.summary, - required this.isHovered, - super.key, - }); - - final SearchSummaryPB summary; - final bool isHovered; - - @override - Widget build(BuildContext context) { - return FlowyHover( - isSelected: () => isHovered, - onHover: (value) { - context.read().add( - SearchResultListEvent.onHoverSummary( - summary: summary, - userHovered: true, - ), - ); - }, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: FlowyText( - summary.content, - maxLines: 20, - ), - ), - ); - } -} - -class SearchSummaryPreview extends StatelessWidget { - const SearchSummaryPreview({ - required this.summary, - super.key, - }); - - final SearchSummaryPB summary; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (summary.highlights.isNotEmpty) ...[ - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_aiOverviewMoreDetails.tr(), - fontSize: 12, - ), - ), - const VSpace(6), - SearchSummaryHighlight(text: summary.highlights), - const VSpace(36), - ], - - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_aiOverviewSource.tr(), - fontSize: 12, - ), - ), - // Sources - const VSpace(6), - ...summary.sources.map((e) => SearchSummarySource(source: e)), - ], - ); - } -} - -class SearchSummaryHighlight extends StatelessWidget { - const SearchSummaryHighlight({ - required this.text, - super.key, - }); - - final String text; - - @override - Widget build(BuildContext context) { - return AIMarkdownText(markdown: text); - } -} - -class SearchSummarySource extends StatelessWidget { - const SearchSummarySource({ - required this.source, - super.key, - }); - - final SearchSourcePB source; - - @override - Widget build(BuildContext context) { - final icon = source.icon.getIcon(); - return FlowyTooltip( - message: LocaleKeys.commandPalette_clickToOpenPage.tr(), - child: SizedBox( - height: 30, - child: FlowyButton( - leftIcon: icon, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - text: FlowyText(source.displayName), - onTap: () { - context.read().add( - SearchResultListEvent.openPage(pageId: source.id), - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 619ee4e229..a8d768aa79 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -52,8 +52,8 @@ class DesktopHomeScreen extends StatelessWidget { return _buildLoading(); } - final workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB, + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB, (error) => null, ); @@ -64,7 +64,7 @@ class DesktopHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceLatest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return const WorkspaceFailedScreen(); } @@ -86,11 +86,11 @@ class DesktopHomeScreen extends StatelessWidget { BlocProvider.value(value: getIt()), BlocProvider( create: (_) => - HomeBloc(workspaceLatest)..add(const HomeEvent.initial()), + HomeBloc(workspaceSetting)..add(const HomeEvent.initial()), ), BlocProvider( create: (_) => HomeSettingBloc( - workspaceLatest, + workspaceSetting, context.read(), context.widthPx, )..add(const HomeSettingEvent.initial()), @@ -137,7 +137,7 @@ class DesktopHomeScreen extends StatelessWidget { child: _buildBody( context, userProfile, - workspaceLatest, + workspaceSetting, ), ), ), @@ -157,7 +157,7 @@ class DesktopHomeScreen extends StatelessWidget { Widget _buildBody( BuildContext context, UserProfilePB userProfile, - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, ) { final layout = HomeLayout(context); final homeStack = HomeStack( @@ -190,7 +190,7 @@ class DesktopHomeScreen extends StatelessWidget { BuildContext context, { required HomeLayout layout, required UserProfilePB userProfile, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, }) { final homeMenu = HomeSideBar( userProfile: userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 464394cd39..ae3b92a702 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -631,7 +631,7 @@ class PageNotifier extends ChangeNotifier { } // Set the plugin view as the latest view. - if (setLatest && newPlugin.id.isNotEmpty) { + if (setLatest) { FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart index f8c3a30488..57168b40a5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -60,7 +60,7 @@ class SidebarTemplateButton extends StatelessWidget { FlowySvgs.icon_template_s, ), text: LocaleKeys.template_label.tr(), - onTap: () => afLaunchUrlString('https://appflowy.com/templates'), + onTap: () => afLaunchUrlString('https://appflowy.io/templates'), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart index 05e6d46957..5e1a6f90e0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart @@ -105,9 +105,9 @@ class SidebarToast extends StatelessWidget { if (role.isOwner) { showSettingsDialog( context, - userProfile: userProfile, - userWorkspaceBloc: userWorkspaceBloc, - initPage: SettingsPage.plan, + userProfile, + userWorkspaceBloc, + SettingsPage.plan, ); } else { final String message; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 67930c336a..559c189925 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -50,8 +50,8 @@ class SidebarTopMenu extends StatelessWidget { } final svgData = Theme.of(context).brightness == Brightness.dark - ? FlowySvgs.app_logo_with_text_dark_xl - : FlowySvgs.app_logo_with_text_light_xl; + ? FlowySvgs.flowy_logo_dark_mode_xl + : FlowySvgs.flowy_logo_text_xl; return Padding( padding: const EdgeInsets.only(top: 12.0, left: 8), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 0bd5dafe91..84a76cfe83 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -2,7 +2,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; @@ -34,7 +33,7 @@ HotKeyItem openSettingsHotKey( ), keyDownHandler: (_) { if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context, userProfile: userProfile); + showSettingsDialog(context, userProfile); } else { Navigator.of(context, rootNavigator: true) .popUntil((route) => route.isFirst); @@ -58,55 +57,37 @@ class UserSettingButton extends StatefulWidget { class _UserSettingButtonState extends State { late UserWorkspaceBloc _userWorkspaceBloc; - late PasswordBloc _passwordBloc; @override void initState() { super.initState(); - _userWorkspaceBloc = context.read(); - _passwordBloc = PasswordBloc(widget.userProfile) - ..add(PasswordEvent.init()) - ..add(PasswordEvent.checkHasPassword()); } @override void didChangeDependencies() { _userWorkspaceBloc = context.read(); - super.didChangeDependencies(); } - @override - void dispose() { - _passwordBloc.close(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return SizedBox.square( dimension: 24.0, child: FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), - child: BlocProvider.value( - value: _passwordBloc, - child: FlowyButton( - onTap: () => showSettingsDialog( - context, - userProfile: widget.userProfile, - userWorkspaceBloc: _userWorkspaceBloc, - passwordBloc: _passwordBloc, - ), - margin: EdgeInsets.zero, - text: FlowySvg( - FlowySvgs.settings_s, - color: widget.isHover - ? Theme.of(context).colorScheme.onSurface - : null, - opacity: 0.7, - ), + child: FlowyButton( + onTap: () => showSettingsDialog( + context, + widget.userProfile, + _userWorkspaceBloc, + ), + margin: EdgeInsets.zero, + text: FlowySvg( + FlowySvgs.settings_s, + color: + widget.isHover ? Theme.of(context).colorScheme.onSurface : null, + opacity: 0.7, ), ), ), @@ -115,33 +96,21 @@ class _UserSettingButtonState extends State { } void showSettingsDialog( - BuildContext context, { - required UserProfilePB userProfile, - UserWorkspaceBloc? userWorkspaceBloc, - PasswordBloc? passwordBloc, + BuildContext context, + UserProfilePB userProfile, [ + UserWorkspaceBloc? bloc, SettingsPage? initPage, -}) { +]) { AFFocusManager.maybeOf(context)?.notifyLoseFocus(); showDialog( context: context, builder: (dialogContext) => MultiBlocProvider( key: _settingsDialogKey, providers: [ - passwordBloc != null - ? BlocProvider.value( - value: passwordBloc, - ) - : BlocProvider( - create: (context) => PasswordBloc(userProfile) - ..add(PasswordEvent.init()) - ..add(PasswordEvent.checkHasPassword()), - ), BlocProvider.value( value: BlocProvider.of(dialogContext), ), - BlocProvider.value( - value: userWorkspaceBloc ?? context.read(), - ), + BlocProvider.value(value: bloc ?? context.read()), ], child: SettingsDialog( userProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 9c19184217..ea55c72f16 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -60,7 +60,7 @@ class HomeSideBar extends StatelessWidget { final UserProfilePB userProfile; - final WorkspaceLatestPB workspaceSetting; + final WorkspaceSettingPB workspaceSetting; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index 95130b029e..d06016dfb8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -13,7 +13,6 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -174,53 +173,42 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { required this.onConfirm, required this.confirmButtonName, this.confirmButtonColor, - this.confirmButtonBuilder, }); final VoidCallback onCancel; final VoidCallback onConfirm; final String confirmButtonName; final Color? confirmButtonColor; - final WidgetBuilder? confirmButtonBuilder; + @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - AFOutlinedTextButton.normal( + OutlinedRoundedButton( text: LocaleKeys.button_cancel.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), onTap: onCancel, ), const HSpace(12.0), - if (confirmButtonBuilder != null) ...[ - confirmButtonBuilder!(context), - ] else ...[ - DecoratedBox( - decoration: ShapeDecoration( - color: - confirmButtonColor ?? Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), - radius: BorderRadius.circular(8), - text: FlowyText.regular( - confirmButtonName, - lineHeight: 1.0, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: onConfirm, + DecoratedBox( + decoration: ShapeDecoration( + color: confirmButtonColor ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), ), - ], + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + radius: BorderRadius.circular(8), + text: FlowyText.regular( + confirmButtonName, + lineHeight: 1.0, + color: Theme.of(context).colorScheme.onPrimary, + ), + onTap: onConfirm, + ), + ), ], ); } @@ -261,11 +249,17 @@ enum ConfirmPopupStyle { class ConfirmPopupColor { static Color titleColor(BuildContext context) { - return AppFlowyTheme.of(context).textColorScheme.primary; + if (Theme.of(context).isLightMode) { + return const Color(0xFF171717).withValues(alpha: 0.8); + } + return const Color(0xFFffffff).withValues(alpha: 0.8); } static Color descriptionColor(BuildContext context) { - return AppFlowyTheme.of(context).textColorScheme.primary; + if (Theme.of(context).isLightMode) { + return const Color(0xFF171717).withValues(alpha: 0.7); + } + return const Color(0xFFffffff).withValues(alpha: 0.7); } } @@ -279,7 +273,6 @@ class ConfirmPopup extends StatefulWidget { this.onCancel, this.confirmLabel, this.confirmButtonColor, - this.confirmButtonBuilder, this.child, this.closeOnAction = true, this.showCloseButton = true, @@ -322,10 +315,6 @@ class ConfirmPopup extends StatefulWidget { /// final bool enableKeyboardListener; - /// Allows to build a custom confirm button. - /// - final WidgetBuilder? confirmButtonBuilder; - @override State createState() => _ConfirmPopupState(); } @@ -379,28 +368,28 @@ class _ConfirmPopupState extends State { } Widget _buildTitle() { - final theme = AppFlowyTheme.of(context); return Row( children: [ Expanded( - child: Text( + child: FlowyText( widget.title, - style: theme.textStyle.heading4.prominent( - color: ConfirmPopupColor.titleColor(context), - ), + fontSize: 16.0, + figmaLineHeight: 22.0, + fontWeight: FontWeight.w500, overflow: TextOverflow.ellipsis, + color: ConfirmPopupColor.titleColor(context), ), ), const HSpace(6.0), if (widget.showCloseButton) ...[ - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), + FlowyButton( + margin: const EdgeInsets.all(3), + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.upgrade_close_s, + size: Size.square(18.0), ), + onTap: () => Navigator.of(context).pop(), ), ], ], @@ -412,24 +401,18 @@ class _ConfirmPopupState extends State { return const SizedBox.shrink(); } - final theme = AppFlowyTheme.of(context); - - return Text( + return FlowyText.regular( widget.description, - style: theme.textStyle.body.standard( - color: ConfirmPopupColor.descriptionColor(context), - ), + fontSize: 16.0, + color: ConfirmPopupColor.descriptionColor(context), maxLines: 5, + figmaLineHeight: 22.0, ); } Widget _buildStyledButton(BuildContext context) { switch (widget.style) { case ConfirmPopupStyle.onlyOk: - if (widget.confirmButtonBuilder != null) { - return widget.confirmButtonBuilder!(context); - } - return SpaceOkButton( onConfirm: () { widget.onConfirm(); @@ -457,7 +440,6 @@ class _ConfirmPopupState extends State { widget.confirmLabel ?? LocaleKeys.space_delete.tr(), confirmButtonColor: widget.confirmButtonColor ?? Theme.of(context).colorScheme.error, - confirmButtonBuilder: widget.confirmButtonBuilder, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 4ff5ccbf67..ff393a8305 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -306,12 +306,9 @@ class _WorkspaceInfo extends StatelessWidget { // Persist and close other tabs when switching workspace, restore tabs for new workspace getIt().add(TabsEvent.switchWorkspace(workspace.workspaceId)); - context.read().add( - UserWorkspaceEvent.openWorkspace( - workspace.workspaceId, - workspace.workspaceAuthType, - ), - ); + context + .read() + .add(UserWorkspaceEvent.openWorkspace(workspace.workspaceId)); PopoverContainer.of(context).closeAll(); } @@ -386,12 +383,7 @@ class _CreateWorkspaceButton extends StatelessWidget { final workspaceBloc = context.read(); await CreateWorkspaceDialog( onConfirm: (name) { - workspaceBloc.add( - UserWorkspaceEvent.createWorkspace( - name, - AuthTypePB.Server, - ), - ); + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); }, ).show(context); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 50ea9d83c7..038ec9f5a6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -169,6 +169,7 @@ class _SidebarWorkspaceState extends State { if (message != null) { showToastNotification( + context, message: message, type: result.fold( (_) => ToastificationType.success, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 22182f7429..ae9059c623 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -632,11 +632,7 @@ class _SingleInnerViewItemState extends State { Widget _buildViewIconButton() { final iconData = widget.view.icon.toEmojiIconData(); final icon = iconData.isNotEmpty - ? RawEmojiIconWidget( - emoji: iconData, - emojiSize: 16.0, - lineHeight: 18.0 / 16.0, - ) + ? RawEmojiIconWidget(emoji: iconData, emojiSize: 16.0) : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); final Widget child = AppFlowyPopover( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 5b531c2f28..d792c54f04 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -211,7 +211,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { ) { final userProfile = context.read().userProfile; // move to feature doesn't support in local mode - if (userProfile.workspaceAuthType != AuthTypePB.Server) { + if (userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart index 2125ea4b66..71dfdde7a9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart @@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -17,29 +16,28 @@ class SettingsAppVersion extends StatelessWidget { Widget build(BuildContext context) { return ApplicationInfo.isUpdateAvailable ? const _UpdateAppSection() - : _buildIsUpToDate(context); + : _buildIsUpToDate(); } - Widget _buildIsUpToDate(BuildContext context) { - final theme = AppFlowyTheme.of(context); + Widget _buildIsUpToDate() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + FlowyText.regular( LocaleKeys.settings_accountPage_isUpToDate.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), + figmaLineHeight: 17, ), const VSpace(4), - Text( - LocaleKeys.settings_accountPage_officialVersion.tr( - namedArgs: { - 'version': ApplicationInfo.applicationVersion, - }, - ), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, + Opacity( + opacity: 0.7, + child: FlowyText.regular( + LocaleKeys.settings_accountPage_officialVersion.tr( + namedArgs: { + 'version': ApplicationInfo.applicationVersion, + }, + ), + fontSize: 12, + figmaLineHeight: 13, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart index 04d078ec0d..5b11e2d139 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart @@ -8,12 +8,13 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; +const _confirmText = 'DELETE MY ACCOUNT'; const _acceptableConfirmTexts = [ 'delete my account', 'deletemyaccount', @@ -43,36 +44,43 @@ class _AccountDeletionButtonState extends State { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); + final textColor = Theme.of(context).brightness == Brightness.light + ? const Color(0xFF4F4F4F) + : const Color(0xFFB0B0B0); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + FlowyText( LocaleKeys.button_deleteAccount.tr(), - style: theme.textStyle.heading4.enhanced( - color: theme.textColorScheme.primary, - ), + fontSize: 14.0, + fontWeight: FontWeight.w500, + figmaLineHeight: 21.0, + color: textColor, ), const VSpace(8), Row( children: [ Expanded( - child: Text( + child: FlowyText.regular( LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(), + fontSize: 12.0, + figmaLineHeight: 13.0, maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ), + color: textColor, ), ), - AFOutlinedTextButton.destructive( - text: LocaleKeys.button_deleteAccount.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.error, - weight: FontWeight.w400, - ), - onTap: () { + FlowyTextButton( + LocaleKeys.button_deleteAccount.tr(), + constraints: const BoxConstraints(minHeight: 32), + padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 10), + fillColor: Colors.transparent, + radius: Corners.s8Border, + hoverColor: + Theme.of(context).colorScheme.error.withValues(alpha: 0.1), + fontColor: Theme.of(context).colorScheme.error, + fontSize: 12, + isDangerous: true, + onPressed: () { isCheckedNotifier.value = false; textEditingController.clear(); @@ -127,8 +135,7 @@ class _AccountDeletionDialog extends StatelessWidget { ), const VSpace(12.0), FlowyTextField( - hintText: - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(), + hintText: _confirmText, controller: controller, ), const VSpace(16), @@ -169,8 +176,7 @@ class _AccountDeletionDialog extends StatelessWidget { bool _isConfirmTextValid(String text) { // don't convert the text to lower case or upper case, // just check if the text is in the list - return _acceptableConfirmTexts.contains(text) || - text == LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(); + return _acceptableConfirmTexts.contains(text); } Future deleteMyAccount( @@ -186,6 +192,7 @@ Future deleteMyAccount( if (!isChecked) { showToastNotification( + context, type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -200,6 +207,7 @@ Future deleteMyAccount( if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) { showToastNotification( + context, type: ToastificationType.warning, bottomPadding: bottomPadding, message: LocaleKeys @@ -217,6 +225,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( + context, message: LocaleKeys .newSettings_myAccount_deleteAccount_deleteAccountSuccess .tr(), @@ -235,6 +244,7 @@ Future deleteMyAccount( loading.stop(); showToastNotification( + context, type: ToastificationType.error, bottomPadding: bottomPadding, message: f.msg, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart index 78f1aaf16e..13f5d832d0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart @@ -3,16 +3,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; import 'package:appflowy/user/application/prelude.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart'; import 'package:appflowy/util/navigator_context_extension.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -32,15 +28,9 @@ class AccountSignInOutSection extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Row( children: [ - Text( - LocaleKeys.settings_accountPage_login_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), + FlowyText.regular(LocaleKeys.settings_accountPage_login_title.tr()), const Spacer(), AccountSignInOutButton( userProfile: userProfile, @@ -66,10 +56,13 @@ class AccountSignInOutButton extends StatelessWidget { @override Widget build(BuildContext context) { - return AFFilledTextButton.primary( + return PrimaryRoundedButton( text: signIn ? LocaleKeys.settings_accountPage_login_loginLabel.tr() : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + fontWeight: FontWeight.w500, + radius: 8.0, onTap: () => signIn ? _showSignInDialog(context) : _showLogoutDialog(context), ); @@ -79,7 +72,9 @@ class AccountSignInOutButton extends StatelessWidget { showConfirmDialog( context: context, title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - description: LocaleKeys.settings_menu_logoutPrompt.tr(), + description: userProfile.encryptionType == EncryptionTypePB.Symmetric + ? LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr() + : LocaleKeys.settings_menu_logoutPrompt.tr(), onConfirm: () async { await getIt().signOut(); onAction(); @@ -101,94 +96,6 @@ class AccountSignInOutButton extends StatelessWidget { } } -class ChangePasswordSection extends StatelessWidget { - const ChangePasswordSection({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - Text( - LocaleKeys.newSettings_myAccount_password_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - state.hasPassword - ? AFFilledTextButton.primary( - text: LocaleKeys - .newSettings_myAccount_password_changePassword - .tr(), - onTap: () => _showChangePasswordDialog(context), - ) - : AFFilledTextButton.primary( - text: LocaleKeys - .newSettings_myAccount_password_setupPassword - .tr(), - onTap: () => _showSetPasswordDialog(context), - ), - ], - ); - }, - ); - } - - Future _showChangePasswordDialog(BuildContext context) async { - final theme = AppFlowyTheme.of(context); - await showDialog( - context: context, - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: getIt(), - ), - ], - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - ), - child: ChangePasswordDialogContent( - userProfile: userProfile, - ), - ), - ), - ); - } - - Future _showSetPasswordDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: getIt(), - ), - ], - child: Dialog( - child: SetupPasswordDialogContent( - userProfile: userProfile, - ), - ), - ), - ); - } -} - class _SignInDialogContent extends StatelessWidget { const _SignInDialogContent(); @@ -204,7 +111,7 @@ class _SignInDialogContent extends StatelessWidget { const _DialogHeader(), const _DialogTitle(), const VSpace(16), - const ContinueWithEmailAndPassword(), + const SignInWithMagicLinkButtons(), if (isAuthEnabled) ...[ const VSpace(20), const _OrDivider(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart index 62a6232c4a..bd08501ae4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart @@ -4,7 +4,6 @@ import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -97,29 +96,27 @@ class _AccountUserProfileState extends State { } Widget _buildNameDisplay() { - final theme = AppFlowyTheme.of(context); return Padding( padding: const EdgeInsets.only(top: 12), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: Text( + child: FlowyText.medium( widget.name, overflow: TextOverflow.ellipsis, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), ), ), const HSpace(4), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), + GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () => setState(() => isEditing = true), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.toolbar_link_edit_m, - size: const Size.square(20), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart deleted file mode 100644 index d606f870ff..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -class SettingsEmailSection extends StatelessWidget { - const SettingsEmailSection({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.settings_accountPage_email_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - VSpace(theme.spacing.s), - Text( - userProfile.email, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart deleted file mode 100644 index 194254c869..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ChangePasswordDialogContent extends StatefulWidget { - const ChangePasswordDialogContent({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - State createState() => - _ChangePasswordDialogContentState(); -} - -class _ChangePasswordDialogContentState - extends State { - final currentPasswordTextFieldKey = GlobalKey(); - final newPasswordTextFieldKey = GlobalKey(); - final confirmPasswordTextFieldKey = GlobalKey(); - - final currentPasswordController = TextEditingController(); - final newPasswordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); - - final iconSize = 20.0; - - @override - void dispose() { - currentPasswordController.dispose(); - newPasswordController.dispose(); - confirmPasswordController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocListener( - listener: _onPasswordStateChanged, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - constraints: const BoxConstraints(maxWidth: 400), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(context), - VSpace(theme.spacing.l), - ..._buildCurrentPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildNewPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildConfirmPasswordFields(context), - VSpace(theme.spacing.l), - _buildSubmitButton(context), - ], - ), - ), - ); - } - - Widget _buildTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Change password', - style: theme.textStyle.heading4.prominent( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), - ), - ), - ], - ); - } - - List _buildCurrentPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_currentPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: currentPasswordTextFieldKey, - controller: currentPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_enterYourCurrentPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildNewPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_newPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: newPasswordTextFieldKey, - controller: newPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_enterYourNewPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - newPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildConfirmPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: confirmPasswordTextFieldKey, - controller: confirmPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_confirmYourNewPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - Widget _buildSubmitButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AFOutlinedTextButton.normal( - text: LocaleKeys.button_cancel.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - weight: FontWeight.w400, - ), - onTap: () => Navigator.of(context).pop(), - ), - const HSpace(16), - AFFilledTextButton.primary( - text: LocaleKeys.button_save.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.onFill, - weight: FontWeight.w400, - ), - onTap: () => _save(context), - ), - ], - ); - } - - void _save(BuildContext context) async { - _resetError(); - - final currentPassword = currentPasswordController.text; - final newPassword = newPasswordController.text; - final confirmPassword = confirmPasswordController.text; - - if (newPassword.isEmpty) { - newPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsRequired - .tr(), - ); - return; - } - - if (confirmPassword.isEmpty) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_confirmPasswordIsRequired - .tr(), - ); - return; - } - - if (newPassword != confirmPassword) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_passwordsDoNotMatch - .tr(), - ); - return; - } - - if (newPassword == currentPassword) { - newPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsSameAsCurrent - .tr(), - ); - return; - } - - // all the verification passed, save the new password - context.read().add( - PasswordEvent.changePassword( - oldPassword: currentPassword, - newPassword: newPassword, - ), - ); - } - - void _resetError() { - currentPasswordTextFieldKey.currentState?.clearError(); - newPasswordTextFieldKey.currentState?.clearError(); - confirmPasswordTextFieldKey.currentState?.clearError(); - } - - void _onPasswordStateChanged(BuildContext context, PasswordState state) { - bool hasError = false; - String message = ''; - String description = ''; - - final changePasswordResult = state.changePasswordResult; - final setPasswordResult = state.setupPasswordResult; - - if (changePasswordResult != null) { - changePasswordResult.fold( - (success) { - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordUpdatedSuccessfully - .tr(); - }, - (error) { - hasError = true; - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordUpdatedFailed - .tr(); - description = error.msg; - }, - ); - } else if (setPasswordResult != null) { - setPasswordResult.fold( - (success) { - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordSetupSuccessfully - .tr(); - }, - (error) { - hasError = true; - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordSetupFailed - .tr(); - description = error.msg; - }, - ); - } - - if (!state.isSubmitting && message.isNotEmpty) { - showToastNotification( - message: message, - description: description, - type: hasError ? ToastificationType.error : ToastificationType.success, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart deleted file mode 100644 index 5417b1a0eb..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class PasswordSuffixIcon extends StatelessWidget { - const PasswordSuffixIcon({ - super.key, - required this.isObscured, - required this.onTap, - }); - - final bool isObscured; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Padding( - padding: EdgeInsets.only(right: theme.spacing.m), - child: GestureDetector( - onTap: onTap, - child: FlowySvg( - isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s, - color: theme.textColorScheme.secondary, - size: const Size.square(20), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart deleted file mode 100644 index 2fdfd8b934..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SetupPasswordDialogContent extends StatefulWidget { - const SetupPasswordDialogContent({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - State createState() => - _SetupPasswordDialogContentState(); -} - -class _SetupPasswordDialogContentState - extends State { - final passwordTextFieldKey = GlobalKey(); - final confirmPasswordTextFieldKey = GlobalKey(); - - final passwordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); - - final iconSize = 20.0; - - @override - void dispose() { - passwordController.dispose(); - confirmPasswordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocListener( - listener: _onPasswordStateChanged, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(context), - VSpace(theme.spacing.l), - ..._buildPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildConfirmPasswordFields(context), - VSpace(theme.spacing.l), - _buildSubmitButton(context), - ], - ), - ), - ); - } - - Widget _buildTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - LocaleKeys.newSettings_myAccount_password_setupPassword.tr(), - style: theme.textStyle.heading4.prominent( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), - ), - ), - ], - ); - } - - List _buildPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - 'Password', - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: passwordTextFieldKey, - controller: passwordController, - hintText: 'Enter your password', - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - passwordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildConfirmPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - 'Confirm password', - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: confirmPasswordTextFieldKey, - controller: confirmPasswordController, - hintText: 'Confirm your password', - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - Widget _buildSubmitButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AFOutlinedTextButton.normal( - text: 'Cancel', - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - weight: FontWeight.w400, - ), - onTap: () => Navigator.of(context).pop(), - ), - const HSpace(16), - AFFilledTextButton.primary( - text: 'Save', - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.onFill, - weight: FontWeight.w400, - ), - onTap: () => _save(context), - ), - ], - ); - } - - void _save(BuildContext context) async { - _resetError(); - - final password = passwordController.text; - final confirmPassword = confirmPasswordController.text; - - if (password.isEmpty) { - passwordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsRequired - .tr(), - ); - return; - } - - if (confirmPassword.isEmpty) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_confirmPasswordIsRequired - .tr(), - ); - return; - } - - if (password != confirmPassword) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_passwordsDoNotMatch - .tr(), - ); - return; - } - - // all the verification passed, save the password - context.read().add( - PasswordEvent.setupPassword( - newPassword: password, - ), - ); - } - - void _resetError() { - passwordTextFieldKey.currentState?.clearError(); - confirmPasswordTextFieldKey.currentState?.clearError(); - } - - void _onPasswordStateChanged(BuildContext context, PasswordState state) { - bool hasError = false; - String message = ''; - String description = ''; - - final setPasswordResult = state.setupPasswordResult; - - if (setPasswordResult != null) { - setPasswordResult.fold( - (success) { - message = 'Password set'; - description = 'Your password has been set'; - }, - (error) { - hasError = true; - message = 'Failed to set password'; - description = error.msg; - }, - ); - } - - if (!state.isSubmitting && message.isNotEmpty) { - showToastNotification( - message: message, - description: description, - type: hasError ? ToastificationType.error : ToastificationType.success, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart new file mode 100644 index 0000000000..a7ed782aea --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart @@ -0,0 +1,119 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; + +class DownloadingIndicator extends StatelessWidget { + const DownloadingIndicator({ + required this.llmModel, + required this.onCancel, + required this.onFinish, + super.key, + }); + final LLMModelPB llmModel; + final VoidCallback onCancel; + final VoidCallback onFinish; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()), + child: BlocListener( + listener: (context, state) { + if (state.isFinish) { + onFinish(); + } + }, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DownloadingProgressBar(onCancel: onCancel), + if (state.bigFileDownloadPrompt != null) ...[ + const VSpace(2), + Opacity( + opacity: 0.6, + child: + FlowyText(state.bigFileDownloadPrompt!, fontSize: 11), + ), + ], + ], + ); + }, + ), + ), + ), + ); + } +} + +class DownloadingProgressBar extends StatelessWidget { + const DownloadingProgressBar({required this.onCancel, super.key}); + + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Opacity( + opacity: 0.6, + child: FlowyText( + "${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}", + fontSize: 11, + ), + ), + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: LinearPercentIndicator( + lineHeight: 9.0, + percent: state.percent, + padding: EdgeInsets.zero, + progressColor: AFThemeExtension.of(context).success, + backgroundColor: + AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(8), + trailing: FlowyText( + "${(state.percent * 100).toStringAsFixed(0)}%", + fontSize: 11, + color: AFThemeExtension.of(context).success, + ), + ), + ), + const HSpace(12), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.button_cancel.tr(), + fontSize: 11, + ), + onTap: onCancel, + ), + ], + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart new file mode 100644 index 0000000000..d924b46825 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart @@ -0,0 +1,78 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class InitLocalAIIndicator extends StatelessWidget { + const InitLocalAIIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: BlocBuilder( + builder: (context, state) { + switch (state.runningState) { + case RunningStatePB.Connecting: + case RunningStatePB.Connected: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoading.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ); + case RunningStatePB.Running: + return SizedBox( + height: 30, + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + ], + ), + ); + case RunningStatePB.Stopped: + return Row( + children: [ + const HSpace(8), + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStopped.tr(), + fontSize: 11, + color: const Color(0xFFC62828), + ), + ], + ); + default: + return const SizedBox.shrink(); + } + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart new file mode 100644 index 0000000000..9cb3a17d88 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart @@ -0,0 +1,370 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class LocalAIChatSetting extends StatelessWidget { + const LocalAIChatSetting({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => LocalAIChatSettingBloc()), + BlocProvider( + create: (context) => LocalAIChatToggleBloc() + ..add(const LocalAIChatToggleEvent.started()), + ), + ], + child: ExpandableNotifier( + child: BlocListener( + listener: (context, state) { + // Listen to the toggle state and expand the panel if the state is ready. + final controller = ExpandableController.of( + context, + required: true, + )!; + + // Neet to wrap with WidgetsBinding.instance.addPostFrameCallback otherwise the + // ExpandablePanel not expanded sometimes. Maybe because the ExpandablePanel is not + // built yet when the listener is called. + WidgetsBinding.instance.addPostFrameCallback( + (_) { + state.pageIndicator.when( + error: (_) => controller.expanded = false, + ready: (enabled) { + controller.expanded = enabled; + context.read().add( + const LocalAIChatSettingEvent.refreshAISetting(), + ); + }, + loading: () => controller.expanded = false, + ); + }, + debugLabel: 'LocalAI.showLocalAIChatSetting', + ); + }, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const SizedBox.shrink(), + collapsed: const SizedBox.shrink(), + expanded: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + // child: _LocalLLMInfoWidget(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocBuilder( + builder: (context, state) { + // If the progress indicator is startOfflineAIApp, then don't show the LLM model. + if (state.progressIndicator == + const LocalAIProgress.startOfflineAIApp()) { + return const SizedBox.shrink(); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FlowyText.medium( + LocaleKeys.settings_aiPage_keys_llmModel.tr(), + fontSize: 14, + ), + ), + const Spacer(), + state.aiModelProgress.when( + init: () => const SizedBox.shrink(), + loading: () { + return const Expanded( + child: Row( + children: [ + Spacer(), + CircularProgressIndicator.adaptive(), + ], + ), + ); + }, + finish: (err) => (err == null) + ? const _SelectLocalModelDropdownMenu() + : const SizedBox.shrink(), + ), + ], + ); + } + }, + ), + const IntrinsicHeight(child: _LocalAIStateWidget()), + ], + ), + ), + ), + ), + ), + ); + } +} + +class LocalAIChatSettingHeader extends StatelessWidget { + const LocalAIChatSettingHeader({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.pageIndicator.when( + error: (error) { + return const SizedBox.shrink(); + }, + loading: () { + return Row( + children: [ + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIStart.tr(), + ), + const Spacer(), + const CircularProgressIndicator.adaptive(), + const HSpace(8), + ], + ); + }, + ready: (isEnabled) { + return Row( + children: [ + const FlowyText('Enable Local AI Chat'), + const Spacer(), + Toggle( + value: isEnabled, + onChanged: (_) { + context + .read() + .add(const LocalAIChatToggleEvent.toggle()); + }, + ), + ], + ); + }, + ); + }, + ); + } +} + +class _SelectLocalModelDropdownMenu extends StatelessWidget { + const _SelectLocalModelDropdownMenu(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Flexible( + child: SettingsDropdown( + key: const Key('_SelectLocalModelDropdownMenu'), + onChanged: (model) => context.read().add( + LocalAIChatSettingEvent.selectLLMConfig(model), + ), + selectedOption: state.selectedLLMModel!, + options: state.models + .map( + (llm) => buildDropdownMenuEntry( + context, + value: llm, + label: llm.chatModel, + ), + ) + .toList(), + ), + ); + }, + ); + } +} + +class _LocalAIStateWidget extends StatelessWidget { + const _LocalAIStateWidget(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final error = errorFromState(state); + if (error == null) { + // If the error is null, handle selected llm model. + if (state.progressIndicator != null) { + final child = state.progressIndicator!.when( + showDownload: ( + LocalModelResourcePB llmResource, + LLMModelPB llmModel, + ) => + _ShowDownloadIndicator( + llmResource: llmResource, + llmModel: llmModel, + ), + startDownloading: (llmModel) { + return DownloadingIndicator( + key: UniqueKey(), + llmModel: llmModel, + onFinish: () => context + .read() + .add(const LocalAIChatSettingEvent.finishDownload()), + onCancel: () => context + .read() + .add(const LocalAIChatSettingEvent.cancelDownload()), + ); + }, + finishDownload: () => const InitLocalAIIndicator(), + checkPluginState: () => const PluginStateIndicator(), + startOfflineAIApp: () => OpenOrDownloadOfflineAIApp( + onRetry: () { + context + .read() + .add(const LocalAIChatSettingEvent.refreshAISetting()); + }, + ), + ); + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: child, + ); + } else { + return const SizedBox.shrink(); + } + } else { + return Opacity( + opacity: 0.5, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: FlowyText( + error.msg, + maxLines: 10, + ), + ), + ); + } + }, + ); + } + + FlowyError? errorFromState(LocalAIChatSettingState state) { + final err = state.aiModelProgress.when( + loading: () => null, + finish: (err) => err, + init: () {}, + ); + + if (err == null) { + state.selectLLMState.when( + loading: () => null, + finish: (err) => err, + ); + } + + return err; + } +} + +void _showDownloadDialog( + BuildContext context, + LocalModelResourcePB llmResource, + LLMModelPB llmModel, +) { + if (llmResource.pendingResources.isEmpty) { + return; + } + + final res = llmResource.pendingResources.first; + String desc = ""; + switch (res.resType) { + case PendingResourceTypePB.AIModel: + desc = LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr( + args: [ + llmResource.pendingResources[0].name, + llmResource.pendingResources[0].fileSize, + ], + ); + break; + case PendingResourceTypePB.OfflineApp: + desc = LocaleKeys.settings_aiPage_keys_downloadAppFlowyOfflineAI.tr(); + break; + } + + showConfirmDialog( + context: context, + style: ConfirmPopupStyle.cancelAndOk, + title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr( + args: [res.name], + ), + description: desc, + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () => context.read().add( + LocalAIChatSettingEvent.startDownloadModel( + llmModel, + ), + ), + onCancel: () => context.read().add( + const LocalAIChatSettingEvent.cancelDownload(), + ), + ); +} + +class _ShowDownloadIndicator extends StatelessWidget { + const _ShowDownloadIndicator({ + required this.llmResource, + required this.llmModel, + }); + final LocalModelResourcePB llmResource; + final LLMModelPB llmModel; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + const Spacer(), + IntrinsicWidth( + child: SizedBox( + height: 30, + child: FlowyButton( + text: FlowyText( + LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(), + fontSize: 14, + color: const Color(0xFF005483), + ), + leftIcon: const FlowySvg( + FlowySvgs.local_model_download_s, + color: Color(0xFF005483), + ), + onTap: () { + _showDownloadDialog(context, llmResource, llmModel); + }, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart index 4992864f99..f9dc6fa4d8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart @@ -1,17 +1,17 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:expandable/expandable.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'ollama_setting.dart'; -import 'plugin_status_indicator.dart'; - class LocalAISetting extends StatefulWidget { const LocalAISetting({super.key}); @@ -20,122 +20,117 @@ class LocalAISetting extends StatefulWidget { } class _LocalAISettingState extends State { - final expandableController = ExpandableController(initialExpanded: false); - - @override - void dispose() { - expandableController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LocalAiPluginBloc(), - child: BlocConsumer( - listener: (context, state) { - expandableController.value = state.isEnabled; - }, - builder: (context, state) { - return ExpandablePanel( - controller: expandableController, - theme: ExpandableThemeData( - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: LocalAiSettingHeader( - isEnabled: state.isEnabled, - ), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: EdgeInsets.only(top: 12), - child: LocalAISettingPanel(), - ), - ); - }, - ), - ); - } -} - -class LocalAiSettingHeader extends StatelessWidget { - const LocalAiSettingHeader({ - super.key, - required this.isEnabled, - }); - - final bool isEnabled; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), - ), - const VSpace(4), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), - maxLines: 3, - fontSize: 12, - ), - ], - ), - ), - Toggle( - value: isEnabled, - onChanged: (value) { - _onToggleChanged(value, context); - }, - ), - ], - ); - } - - void _onToggleChanged(bool value, BuildContext context) { - if (value) { - context.read().add(const LocalAiPluginEvent.toggle()); - } else { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), - description: - LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () { - context - .read() - .add(const LocalAiPluginEvent.toggle()); - }, - ); - } - } -} - -class LocalAISettingPanel extends StatelessWidget { - const LocalAISettingPanel({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - if (state is! ReadyLocalAiPluginState) { + if (state.aiSettings == null) { return const SizedBox.shrink(); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const LocalAIStatusIndicator(), - const VSpace(10), - OllamaSettingPage(), - ], + return BlocProvider( + create: (context) => + LocalAIToggleBloc()..add(const LocalAIToggleEvent.started()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: ExpandableNotifier( + child: BlocListener( + listener: (context, state) { + final controller = + ExpandableController.of(context, required: true)!; + state.pageIndicator.when( + error: (_) => controller.expanded = false, + ready: (enabled) => controller.expanded = enabled, + loading: () => controller.expanded = false, + ); + }, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: false, + hasIcon: false, + tapBodyToExpand: false, + tapHeaderToExpand: false, + ), + header: const LocalAISettingHeader(), + collapsed: const SizedBox.shrink(), + expanded: Column( + children: [ + const VSpace(6), + DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + borderRadius: + const BorderRadius.all(Radius.circular(4)), + ), + child: const Padding( + padding: + EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: LocalAIChatSetting(), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +class LocalAISettingHeader extends StatelessWidget { + const LocalAISettingHeader({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.pageIndicator.when( + error: (error) { + return const SizedBox.shrink(); + }, + loading: () { + return const CircularProgressIndicator.adaptive(); + }, + ready: (isEnabled) { + return Row( + children: [ + FlowyText( + LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), + ), + const Spacer(), + Toggle( + value: isEnabled, + onChanged: (_) { + if (isEnabled) { + showConfirmDialog( + context: context, + title: LocaleKeys + .settings_aiPage_keys_disableLocalAITitle + .tr(), + description: LocaleKeys + .settings_aiPage_keys_disableLocalAIDescription + .tr(), + confirmLabel: LocaleKeys.button_confirm.tr(), + onConfirm: () => context + .read() + .add(const LocalAIToggleEvent.toggle()), + ); + } else { + context + .read() + .add(const LocalAIToggleEvent.toggle()); + } + }, + ), + ], + ); + }, ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart deleted file mode 100644 index e90c42444f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalSettingsAIView extends StatelessWidget { - const LocalSettingsAIView({ - super.key, - required this.userProfile, - required this.workspaceId, - }); - - final UserProfilePB userProfile; - final String workspaceId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SettingsAIBloc(userProfile, workspaceId) - ..add(const SettingsAIEvent.started()), - child: SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: "", - children: [ - const LocalAISetting(), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart index 7357c2951c..dfc53e4f08 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart @@ -1,5 +1,3 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -12,54 +10,36 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class AIModelSelection extends StatelessWidget { const AIModelSelection({super.key}); - static const double height = 49; @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (previous, current) => - previous.availableModels != current.availableModels, builder: (context, state) { - final models = state.availableModels?.models; - if (models == null) { - return const SizedBox( - // Using same height as SettingsDropdown to avoid layout shift - height: height, - ); - } - - final localModels = models.where((model) => model.isLocal).toList(); - final cloudModels = models.where((model) => !model.isLocal).toList(); - final selectedModel = state.availableModels!.selectedModel; - return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( + Flexible( child: FlowyText.medium( LocaleKeys.settings_aiPage_keys_llmModelType.tr(), - overflow: TextOverflow.ellipsis, + fontSize: 14, ), ), + const Spacer(), Flexible( - child: SettingsDropdown( + child: SettingsDropdown( key: const Key('_AIModelSelection'), onChanged: (model) => context .read() .add(SettingsAIEvent.selectModel(model)), - selectedOption: selectedModel, - selectOptionCompare: (left, right) => - left?.name == right?.name, - options: [...localModels, ...cloudModels] + selectedOption: state.selectedAIModel, + options: state.availableModels .map( - (model) => buildDropdownMenuEntry( + (model) => buildDropdownMenuEntry( context, value: model, - label: - model.isLocal ? "${model.i18n} 🔐" : model.i18n, - subLabel: model.desc, - maximumHeight: height, + label: model, ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart deleted file mode 100644 index 6f38043927..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class OllamaSettingPage extends StatelessWidget { - const OllamaSettingPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - OllamaSettingBloc()..add(const OllamaSettingEvent.started()), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.inputItems != current.inputItems || - previous.isEdited != current.isEdited, - builder: (context, state) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 10, - children: [ - for (final item in state.inputItems) - _SettingItemWidget(item: item), - _SaveButton(isEdited: state.isEdited), - ], - ), - ); - }, - ), - ); - } -} - -class _SettingItemWidget extends StatelessWidget { - const _SettingItemWidget({required this.item}); - - final SettingItem item; - - @override - Widget build(BuildContext context) { - return Column( - key: ValueKey(item.content + item.settingType.title), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - item.settingType.title, - fontSize: 12, - figmaLineHeight: 16, - ), - const VSpace(4), - SizedBox( - height: 32, - child: FlowyTextField( - autoFocus: false, - hintText: item.hintText, - text: item.content, - onChanged: (content) { - context.read().add( - OllamaSettingEvent.onEdit(content, item.settingType), - ); - }, - ), - ), - ], - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({required this.isEdited}); - - final bool isEdited; - - @override - Widget build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.centerEnd, - child: FlowyTooltip( - message: isEdited ? null : 'No changes', - child: SizedBox( - child: FlowyButton( - text: FlowyText( - 'Apply', - figmaLineHeight: 20, - color: Theme.of(context).colorScheme.onPrimary, - ), - disable: !isEdited, - expandText: false, - margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200), - onTap: () { - if (isEdited) { - context - .read() - .add(const OllamaSettingEvent.submit()); - } - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart new file mode 100644 index 0000000000..bf601b6184 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart @@ -0,0 +1,255 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart'; +import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PluginStateIndicator extends StatelessWidget { + const PluginStateIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + PluginStateBloc()..add(const PluginStateEvent.started()), + child: BlocBuilder( + builder: (context, state) { + return state.action.when( + init: () => const _InitPlugin(), + ready: () => const _LocalAIReadyToUse(), + restartPlugin: () => const _ReloadButton(), + loadingPlugin: () => const _InitPlugin(), + startAIOfflineApp: () => OpenOrDownloadOfflineAIApp( + onRetry: () { + context + .read() + .add(const PluginStateEvent.started()); + }, + ), + ); + }, + ), + ); + } +} + +class _InitPlugin extends StatelessWidget { + const _InitPlugin(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()), + const Spacer(), + const SizedBox( + height: 20, + child: CircularProgressIndicator.adaptive(), + ), + ], + ); + } +} + +class _ReloadButton extends StatelessWidget { + const _ReloadButton(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const FlowySvg( + FlowySvgs.download_warn_s, + color: Color(0xFFC62828), + ), + const HSpace(6), + FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()), + const Spacer(), + SizedBox( + height: 30, + child: FlowyButton( + useIntrinsicWidth: true, + text: + FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()), + onTap: () { + context.read().add( + const PluginStateEvent.restartLocalAI(), + ); + }, + ), + ), + ], + ); + } +} + +class _LocalAIReadyToUse extends StatelessWidget { + const _LocalAIReadyToUse(); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + color: Color(0xFFEDF7ED), + borderRadius: BorderRadius.all( + Radius.circular(4), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + children: [ + const HSpace(8), + const FlowySvg( + FlowySvgs.download_success_s, + color: Color(0xFF2E7D32), + ), + const HSpace(6), + Flexible( + child: FlowyText( + LocaleKeys.settings_aiPage_keys_localAILoaded.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + maxLines: 3, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.settings_aiPage_keys_openModelDirectory.tr(), + fontSize: 11, + color: const Color(0xFF1E4620), + ), + onTap: () { + context.read().add( + const PluginStateEvent.openModelDirectory(), + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class OpenOrDownloadOfflineAIApp extends StatelessWidget { + const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key}); + + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DownloadOfflineAIBloc(), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: 3, + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString( + "https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline", + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + "${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + height: 1.5, + ), + recognizer: TapGestureRecognizer() + ..onTap = + () => context.read().add( + const DownloadOfflineAIEvent.started(), + ), + ), + TextSpan( + text: + " ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(height: 1.5), + ), + ], + ), + ), + // const SizedBox( + // height: 6, + // ), // Replaced VSpace with SizedBox for simplicity + // SizedBox( + // height: 30, + // child: FlowyButton( + // useIntrinsicWidth: true, + // margin: const EdgeInsets.symmetric(horizontal: 12), + // text: FlowyText( + // LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(), + // ), + // onTap: onRetry, + // ), + // ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart deleted file mode 100644 index a280cf0644..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart +++ /dev/null @@ -1,363 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalAIStatusIndicator extends StatelessWidget { - const LocalAIStatusIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - ready: (_, version, runningState, lackOfResource) { - if (lackOfResource != null) { - return _LackOfResource(resource: lackOfResource); - } - - return switch (runningState) { - RunningStatePB.ReadyToRun => const _ReadyToRun(), - RunningStatePB.Connecting || - RunningStatePB.Connected => - _Initializing(), - RunningStatePB.Running => _LocalAIRunning(version: version), - RunningStatePB.Stopped => const _RestartPluginButton(), - _ => const SizedBox.shrink(), - }; - }, - orElse: () => const SizedBox.shrink(), - ); - }, - ); - } -} - -class _ReadyToRun extends StatelessWidget { - const _ReadyToRun(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const SizedBox.square( - dimension: 20.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - strokeAlign: BorderSide.strokeAlignInside, - ), - ), - const HSpace(8.0), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -class _Initializing extends StatelessWidget { - const _Initializing(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const SizedBox.square( - dimension: 20.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - strokeAlign: BorderSide.strokeAlignInside, - ), - ), - HSpace(8), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -class _RestartPluginButton extends StatelessWidget { - const _RestartPluginButton(); - - @override - Widget build(BuildContext context) { - final textStyle = - Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.toast_error_filled_s, - size: Size.square(20.0), - blendMode: null, - ), - const HSpace(8), - Expanded( - child: RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: - LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - context - .read() - .add(const LocalAiPluginEvent.restart()); - }, - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning({ - required this.version, - }); - - final String version; - - @override - Widget build(BuildContext context) { - final runningText = LocaleKeys.settings_aiPage_keys_localAIRunning.tr(); - final text = version.isEmpty ? runningText : "$runningText ($version)"; - - return Container( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - Expanded( - child: FlowyText( - text, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ), - ); - } -} - -class _LackOfResource extends StatelessWidget { - const _LackOfResource({required this.resource}); - - final LackOfAIResourcePB resource; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - FlowySvg( - FlowySvgs.toast_error_filled_s, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(8), - Expanded( - child: switch (resource.resourceType) { - LackOfAIResourceTypePB.PluginExecutableNotReady => - _buildNoLAI(context), - LackOfAIResourceTypePB.OllamaServerNotReady => - _buildNoOllama(context), - LackOfAIResourceTypePB.MissingModel => - _buildNoModel(context, resource.missingModelNames), - _ => const SizedBox.shrink(), - }, - ), - ], - ), - ); - } - - TextStyle? _textStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - } - - Widget _buildNoLAI(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: _textStyle(context)), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoOllama(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: textStyle), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoModel(BuildContext context, List modelNames) { - final textStyle = _textStyle(context); - - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), - style: textStyle, - ), - TextSpan( - text: modelNames.join(', '), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_instructions.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_downloadModel.tr(), - style: textStyle, - ), - ], - ), - ); - } - - List _downloadInstructions(TextStyle? textStyle) { - return [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_instructions.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - TextSpan(text: ' ', style: textStyle), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_installOllamaLai.tr(), - style: textStyle, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index c2e75ff2f2..a6181ba016 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -1,15 +1,39 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart'; import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget { + const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30), + child: FlowyText( + LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(), + maxLines: null, + fontSize: 16, + lineHeight: 1.6, + ), + ); + } +} + class SettingsAIView extends StatelessWidget { const SettingsAIView({ super.key, @@ -25,16 +49,34 @@ class SettingsAIView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SettingsAIBloc(userProfile, workspaceId) - ..add(const SettingsAIEvent.started()), - child: SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: [ - const AIModelSelection(), - const _AISearchToggle(value: false), - const LocalAISetting(), - ], + create: (_) => + SettingsAIBloc(userProfile, workspaceId, currentWorkspaceMemberRole) + ..add(const SettingsAIEvent.started()), + child: BlocBuilder( + builder: (context, state) { + final children = [ + const AIModelSelection(), + ]; + + children.add(const _AISearchToggle(value: false)); + + if (state.currentWorkspaceMemberRole != null) { + children.add( + _LocalAIOnBoarding( + userProfile: userProfile, + currentWorkspaceMemberRole: state.currentWorkspaceMemberRole!, + workspaceId: workspaceId, + ), + ); + } + + return SettingsBody( + title: LocaleKeys.settings_aiPage_title.tr(), + description: + LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), + children: children, + ); + }, ), ); } @@ -82,3 +124,122 @@ class _AISearchToggle extends StatelessWidget { ); } } + +// ignore: unused_element +class _LocalAIOnBoarding extends StatelessWidget { + const _LocalAIOnBoarding({ + required this.userProfile, + required this.currentWorkspaceMemberRole, + required this.workspaceId, + }); + final UserProfilePB userProfile; + final AFRolePB? currentWorkspaceMemberRole; + final String workspaceId; + + @override + Widget build(BuildContext context) { + if (FeatureFlag.planBilling.isOn) { + return BillingGateGuard( + builder: (context) { + return BlocProvider( + create: (context) => LocalAIOnBoardingBloc( + userProfile, + currentWorkspaceMemberRole, + workspaceId, + )..add(const LocalAIOnBoardingEvent.started()), + child: BlocBuilder( + builder: (context, state) { + // Show the local AI settings if the user has purchased the AI Local plan + if (kDebugMode || state.isPurchaseAILocal) { + return const LocalAISetting(); + } else { + if (currentWorkspaceMemberRole?.isOwner ?? false) { + // Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan + return _UpgradeToAILocalPlan( + onTap: () { + context.read().add( + const LocalAIOnBoardingEvent.addSubscription( + SubscriptionPlanPB.AiLocal, + ), + ); + }, + ); + } else { + return const _AskOwnerUpgradeToLocalAI(); + } + } + }, + ), + ); + }, + ); + } else { + return const SizedBox.shrink(); + } + } +} + +class _AskOwnerUpgradeToLocalAI extends StatelessWidget { + const _AskOwnerUpgradeToLocalAI(); + + @override + Widget build(BuildContext context) { + return FlowyText( + LocaleKeys.sideBar_askOwnerToUpgradeToLocalAI.tr(), + color: AFThemeExtension.of(context).strongText, + ); + } +} + +class _UpgradeToAILocalPlan extends StatefulWidget { + const _UpgradeToAILocalPlan({required this.onTap}); + + final VoidCallback onTap; + + @override + State<_UpgradeToAILocalPlan> createState() => _UpgradeToAILocalPlanState(); +} + +class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> { + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + LocaleKeys.sideBar_upgradeToAILocal.tr(), + maxLines: 10, + lineHeight: 1.5, + ), + const VSpace(4), + Opacity( + opacity: 0.6, + child: FlowyText( + LocaleKeys.sideBar_upgradeToAILocalDesc.tr(), + fontSize: 12, + maxLines: 10, + lineHeight: 1.5, + ), + ), + ], + ), + ), + BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator.adaptive(); + } else { + return Toggle( + value: false, + onChanged: (_) => widget.onTap(), + ); + } + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index d7afb03e87..701d1cb565 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -4,12 +4,12 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart'; import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -45,11 +45,11 @@ class _SettingsAccountViewState extends State { child: BlocBuilder( builder: (context, state) { return SettingsBody( - title: LocaleKeys.newSettings_myAccount_title.tr(), + title: LocaleKeys.settings_accountPage_title.tr(), children: [ // user profile SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myProfile.tr(), + title: LocaleKeys.settings_accountPage_general_title.tr(), children: [ AccountUserProfile( name: userName, @@ -61,7 +61,7 @@ class _SettingsAccountViewState extends State { setState(() => userName = newName); context .read() - .add(SettingsUserEvent.updateUserName(name: newName)); + .add(SettingsUserEvent.updateUserName(newName)); }, ), ], @@ -70,42 +70,37 @@ class _SettingsAccountViewState extends State { // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myAccount.tr(), + title: LocaleKeys.settings_accountPage_email_title.tr(), children: [ - SettingsEmailSection( - userProfile: state.userProfile, - ), - ChangePasswordSection( - userProfile: state.userProfile, - ), + FlowyText.regular(state.userProfile.email), AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.workspaceAuthType == - AuthTypePB.Local + onAction: state.userProfile.authenticator == + AuthenticatorPB.Local ? widget.didLogin : widget.didLogout, - signIn: state.userProfile.workspaceAuthType == - AuthTypePB.Local, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, ), ], ), ], if (isAuthEnabled && - state.userProfile.workspaceAuthType == AuthTypePB.Local) ...[ + state.userProfile.authenticator == AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_accountPage_login_title.tr(), children: [ AccountSignInOutSection( userProfile: state.userProfile, - onAction: state.userProfile.workspaceAuthType == - AuthTypePB.Local + onAction: state.userProfile.authenticator == + AuthenticatorPB.Local ? widget.didLogin : widget.didLogout, - signIn: state.userProfile.workspaceAuthType == - AuthTypePB.Local, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, ), ], ), @@ -120,7 +115,8 @@ class _SettingsAccountViewState extends State { ), // user deletion - if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) + if (widget.userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) const AccountDeletionButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart index 77c1116319..450f2167de 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/shared/loading.dart'; import 'package:appflowy/util/int64_extension.dart'; @@ -209,6 +211,26 @@ class _SettingsBillingViewState extends State { ), ), const SettingsDashedDivider(), + + // Currently, the AI Local tile is only available on macOS + // TODO(nathan): enable windows and linux + if (Platform.isMacOS) + _AITile( + plan: SubscriptionPlanPB.AiLocal, + label: LocaleKeys + .settings_billingPage_addons_aiOnDevice_label + .tr(), + description: LocaleKeys + .settings_billingPage_addons_aiOnDevice_description, + activeDescription: LocaleKeys + .settings_billingPage_addons_aiOnDevice_activeDescription, + canceledDescription: LocaleKeys + .settings_billingPage_addons_aiOnDevice_canceledDescription, + subscriptionInfo: + state.subscriptionInfo.addOns.firstWhereOrNull( + (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal, + ), + ), ], ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index a2d911ea40..7c096b4b2f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -157,6 +157,7 @@ class SettingsManageDataView extends StatelessWidget { if (context.mounted) { showToastNotification( + context, message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 21896ead0e..bf1e479226 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/colors.dart'; @@ -132,6 +134,46 @@ class _SettingsPlanViewState extends State { ), ), const HSpace(8), + + // Currently, the AI Local tile is only available on macOS + // TODO(nathan): enable windows and linux + if (Platform.isMacOS) + Flexible( + child: _AddOnBox( + title: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_title + .tr(), + description: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_description + .tr(), + price: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_price + .tr( + args: [ + SubscriptionPlanPB.AiLocal.priceAnnualBilling, + ], + ), + priceInfo: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_priceInfo + .tr(), + recommend: LocaleKeys + .settings_planPage_planUsage_addons_aiOnDevice_recommend + .tr( + args: [ + SubscriptionPlanPB.AiLocal.priceMonthBilling, + ], + ), + buttonText: state.subscriptionInfo.hasAIOnDevice + ? LocaleKeys + .settings_planPage_planUsage_addons_activeLabel + .tr() + : LocaleKeys + .settings_planPage_planUsage_addons_addLabel + .tr(), + isActive: state.subscriptionInfo.hasAIOnDevice, + plan: SubscriptionPlanPB.AiLocal, + ), + ), ], ), ], @@ -396,6 +438,23 @@ class _PlanUsageSummary extends StatelessWidget { }, ), ], + if (!subscriptionInfo.hasAIOnDevice) ...[ + _ToggleMore( + value: false, + label: LocaleKeys.settings_planPage_planUsage_aiOnDeviceToggle + .tr(), + badgeLabel: + LocaleKeys.settings_planPage_planUsage_aiOnDeviceBadge.tr(), + onTap: () async { + context.read().add( + const SettingsPlanEvent.addSubscription( + SubscriptionPlanPB.AiLocal, + ), + ); + await Future.delayed(const Duration(seconds: 2), () {}); + }, + ), + ], ], ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart index 0d3716c7dc..3552205ba4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -7,7 +7,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; @@ -22,6 +21,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 78ffd34eef..1cfc833398 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -88,7 +88,7 @@ class SettingsWorkspaceView extends StatelessWidget { autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), @@ -180,7 +180,7 @@ class SettingsWorkspaceView extends StatelessWidget { ), const SettingsCategorySpacer(), - if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart index 2f03fc052c..f764bec9e7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart @@ -53,7 +53,8 @@ class SettingsPageSitesEvent { ); getIt().setData(ClipboardServiceData(plainText: url)); showToastNotification( - message: LocaleKeys.message_copy_success.tr(), + context, + message: LocaleKeys.grid_url_copy.tr(), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart index b1d9b9cdae..8f9df5f1b6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart @@ -253,6 +253,7 @@ class _FreePlanUpgradeButton extends StatelessWidget { onTap: () { if (isOwner) { showToastNotification( + context, message: LocaleKeys.settings_sites_namespace_redirectToPayment.tr(), type: ToastificationType.info, @@ -263,6 +264,7 @@ class _FreePlanUpgradeButton extends StatelessWidget { ); } else { showToastNotification( + context, message: LocaleKeys .settings_sites_namespace_pleaseAskOwnerToSetHomePage .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart index 9617f2c8d6..6555494144 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart @@ -216,6 +216,7 @@ class _DomainSettingsDialogState extends State { result.fold( (s) { showToastNotification( + context, message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(), ); @@ -233,6 +234,7 @@ class _DomainSettingsDialogState extends State { Log.error('Failed to update namespace: $f'); showToastNotification( + context, message: basicErrorMessage, type: ToastificationType.error, description: errorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart index ad37bae866..b7f3cecebf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart @@ -203,6 +203,7 @@ class _PublishedViewSettingsDialogState result.fold( (s) { showToastNotification( + context, message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), ); Navigator.of(context).pop(); @@ -211,6 +212,7 @@ class _PublishedViewSettingsDialogState Log.error('update path name failed: $f'); showToastNotification( + context, message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), type: ToastificationType.error, description: f.code.publishErrorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart index f3845b0896..7b00e652ed 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart @@ -178,6 +178,7 @@ class _SettingsSitesPageView extends StatelessWidget { Log.error('Failed to generate payment link for Pro Plan: ${f.msg}'); showToastNotification( + context, message: LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(), type: ToastificationType.error, @@ -187,12 +188,14 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((_) { showToastNotification( + context, message: LocaleKeys.publish_unpublishSuccessfully.tr(), ); }, (f) { Log.error('Failed to unpublish view: ${f.msg}'); showToastNotification( + context, message: LocaleKeys.publish_unpublishFailed.tr(), type: ToastificationType.error, description: f.msg, @@ -201,12 +204,14 @@ class _SettingsSitesPageView extends StatelessWidget { } else if (type == SettingsSitesActionType.setHomePage && result != null) { result.fold((s) { showToastNotification( + context, message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(), ); }, (f) { Log.error('Failed to set homepage: ${f.msg}'); showToastNotification( + context, message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(), type: ToastificationType.error, ); @@ -215,12 +220,14 @@ class _SettingsSitesPageView extends StatelessWidget { result != null) { result.fold((s) { showToastNotification( + context, message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(), ); }, (f) { Log.error('Failed to remove homepage: ${f.msg}'); showToastNotification( + context, message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(), type: ToastificationType.error, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index cd33c62090..5165744627 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -32,7 +32,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'pages/setting_ai_view/local_settings_ai_view.dart'; import 'widgets/setting_cloud.dart'; @visibleForTesting @@ -140,19 +139,15 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.shortcuts: return const SettingsShortcutsView(); case SettingsPage.ai: - if (user.workspaceAuthType == AuthTypePB.Server) { + if (user.authenticator == AuthenticatorPB.AppFlowyCloud) { return SettingsAIView( - key: ValueKey(workspaceId), + key: ValueKey(user.hashCode), userProfile: user, currentWorkspaceMemberRole: currentWorkspaceMemberRole, workspaceId: workspaceId, ); } else { - return LocalSettingsAIView( - key: ValueKey(workspaceId), - userProfile: user, - workspaceId: workspaceId, - ); + return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud(); } case SettingsPage.member: return WorkspaceMembersPage( @@ -368,6 +363,7 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { }) async { if (cloudUrl.isEmpty || webUrl.isEmpty) { showToastNotification( + context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -379,6 +375,7 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { if (mounted) { if (isValid) { showToastNotification( + context, message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]), ); @@ -390,6 +387,7 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { await runAppFlowy(); } else { showToastNotification( + context, message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), type: ToastificationType.error, ); @@ -524,6 +522,7 @@ class _SupportSettings extends StatelessWidget { await getIt().clearAllCache(); if (context.mounted) { showToastNotification( + context, message: LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index 720f7793f2..d005901cff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -9,40 +9,14 @@ DropdownMenuEntry buildDropdownMenuEntry( BuildContext context, { required T value, required String label, - String subLabel = '', T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, String? fontFamily, - double maximumHeight = 29, }) { final fontFamilyUsed = fontFamily != null ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily : defaultFontFamily; - Widget? labelWidget; - if (subLabel.isNotEmpty) { - labelWidget = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - label, - fontSize: 14, - ), - const VSpace(4), - FlowyText.regular( - subLabel, - fontSize: 10, - ), - ], - ); - } else { - labelWidget = FlowyText.regular( - label, - fontSize: 14, - textAlign: TextAlign.start, - fontFamily: fontFamilyUsed, - ); - } return DropdownMenuEntry( style: ButtonStyle( @@ -52,12 +26,17 @@ DropdownMenuEntry buildDropdownMenuEntry( const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), - maximumSize: WidgetStatePropertyAll(Size(double.infinity, maximumHeight)), + maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), ), value: value, label: label, leadingIcon: leadingWidget, - labelWidget: labelWidget, + labelWidget: FlowyText.regular( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ), trailingIcon: Row( children: [ if (trailingWidget != null) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart index 6c8eeb9ae4..5d1c858d29 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart @@ -12,8 +12,6 @@ class SettingValueDropDown extends StatefulWidget { this.child, this.popoverController, this.offset, - this.boxConstraints, - this.margin = const EdgeInsets.all(6), }); final String currentValue; @@ -23,8 +21,6 @@ class SettingValueDropDown extends StatefulWidget { final Widget? child; final PopoverController? popoverController; final Offset? offset; - final BoxConstraints? boxConstraints; - final EdgeInsets margin; @override State createState() => _SettingValueDropDownState(); @@ -37,14 +33,12 @@ class _SettingValueDropDownState extends State { key: widget.popoverKey, controller: widget.popoverController, direction: PopoverDirection.bottomWithCenterAligned, - margin: widget.margin, popupBuilder: widget.popupBuilder, - constraints: widget.boxConstraints ?? - const BoxConstraints( - minWidth: 80, - maxWidth: 160, - maxHeight: 400, - ), + constraints: const BoxConstraints( + minWidth: 80, + maxWidth: 160, + maxHeight: 400, + ), offset: widget.offset, onClose: widget.onClose, child: widget.child ?? diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart index 5114218041..8091a72684 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -1,21 +1,20 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SettingsBody extends StatelessWidget { const SettingsBody({ super.key, required this.title, this.description, - this.descriptionBuilder, this.autoSeparate = true, required this.children, }); final String title; final String? description; - final WidgetBuilder? descriptionBuilder; final bool autoSeparate; final List children; @@ -28,12 +27,7 @@ class SettingsBody extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsHeader( - title: title, - description: description, - descriptionBuilder: descriptionBuilder, - ), - SettingsCategorySpacer(), + SettingsHeader(title: title, description: description), Flexible( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index 33c81b99e8..a111fa2626 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -26,18 +25,15 @@ class SettingsCategory extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Text( + FlowyText.semibold( title, - style: theme.textStyle.heading4.enhanced( - color: theme.textColorScheme.primary, - ), maxLines: 2, + fontSize: 16, overflow: TextOverflow.ellipsis, ), if (tooltip != null) ...[ @@ -51,7 +47,7 @@ class SettingsCategory extends StatelessWidget { if (actions != null) ...actions!, ], ), - const VSpace(16), + const VSpace(8), if (description?.isNotEmpty ?? false) ...[ FlowyText.regular( description!, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index 1ef7f13d0c..5637fdd20c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -1,4 +1,3 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; /// This is used to create a uniform space and divider @@ -8,11 +7,6 @@ class SettingsCategorySpacer extends StatelessWidget { const SettingsCategorySpacer({super.key}); @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Divider( - height: theme.spacing.xl * 2.0, - color: theme.borderColorScheme.primary, - ); - } + Widget build(BuildContext context) => + const Divider(height: 32, color: Color(0xFFF2F2F2)); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index e392ed91f0..3b2e883210 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -16,11 +16,9 @@ class SettingsDropdown extends StatefulWidget { this.onChanged, this.actions, this.expandWidth = true, - this.selectOptionCompare, }); final T selectedOption; - final CompareFunction? selectOptionCompare; final List> options; final void Function(T)? onChanged; final List? actions; @@ -54,7 +52,6 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, - selectOptionCompare: widget.selectOptionCompare, textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( fontFamily: fontFamilyUsed, fontWeight: FontWeight.w400, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart index 332b25e686..c028e6886d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -1,46 +1,32 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + /// Renders a simple header for the settings view /// class SettingsHeader extends StatelessWidget { - const SettingsHeader({ - super.key, - required this.title, - this.description, - this.descriptionBuilder, - }); + const SettingsHeader({super.key, required this.title, this.description}); final String title; final String? description; - final WidgetBuilder? descriptionBuilder; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: theme.textStyle.heading2.enhanced( - color: theme.textColorScheme.primary, - ), - ), - if (descriptionBuilder != null) ...[ - VSpace(theme.spacing.xs), - descriptionBuilder!(context), - ] else if (description?.isNotEmpty == true) ...[ - VSpace(theme.spacing.xs), - Text( + FlowyText.semibold(title, fontSize: 24), + if (description?.isNotEmpty == true) ...[ + const VSpace(8), + FlowyText( description!, maxLines: 4, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ), + fontSize: 12, + color: AFThemeExtension.of(context).secondaryTextColor, ), ], + const VSpace(16), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart new file mode 100644 index 0000000000..ab952386bd --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +SelectionMenuItem emojiMenuItem = SelectionMenuItem( + getName: LocaleKeys.document_plugins_emoji.tr, + icon: (editorState, onSelected, style) => SelectableIconWidget( + icon: Icons.emoji_emotions_outlined, + isSelected: onSelected, + style: style, + ), + keywords: ['emoji'], + handler: (editorState, menuService, context) { + final container = Overlay.of(context); + menuService.dismiss(); + showEmojiPickerMenu( + container, + editorState, + menuService.alignment, + menuService.offset, + ); + }, +); + +void showEmojiPickerMenu( + OverlayState container, + EditorState editorState, + Alignment alignment, + Offset offset, +) { + final top = alignment == Alignment.topLeft ? offset.dy : null; + final bottom = alignment == Alignment.bottomLeft ? offset.dy : null; + + keepEditorFocusNotifier.increase(); + late OverlayEntry emojiPickerMenuEntry; + emojiPickerMenuEntry = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.decrease(), + builder: (context) => Material( + type: MaterialType.transparency, + child: Container( + width: 360, + height: 380, + padding: const EdgeInsets.all(4.0), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + ), + child: EmojiSelectionMenu( + onSubmitted: (emoji) { + editorState.insertTextAtCurrentSelection(emoji); + }, + onExit: () { + // close emoji panel + emojiPickerMenuEntry.remove(); + }, + ), + ), + ), + ).build(); + container.insert(emojiPickerMenuEntry); +} + +class EmojiSelectionMenu extends StatefulWidget { + const EmojiSelectionMenu({ + super.key, + required this.onSubmitted, + required this.onExit, + }); + + final void Function(String emoji) onSubmitted; + final void Function() onExit; + + @override + State createState() => _EmojiSelectionMenuState(); +} + +class _EmojiSelectionMenuState extends State { + @override + void initState() { + super.initState(); + HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); + } + + bool _handleGlobalKeyEvent(KeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.escape && + event is KeyDownEvent) { + //triggers on esc + widget.onExit(); + return true; + } + return false; + } + + @override + void deactivate() { + HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + return FlowyEmojiPicker( + onEmojiSelected: (r) => widget.onSubmitted(r.emoji), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart index 6e1f6e239f..a369cc6b87 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart @@ -1,3 +1,4 @@ +export 'emoji_menu_item.dart'; export 'emoji_shortcut_event.dart'; export 'src/emji_picker_config.dart'; export 'src/emoji_picker.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index 6959f69788..078cf64963 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -1,7 +1,5 @@ -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; -import 'package:appflowy/plugins/emoji/emoji_menu.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( @@ -17,16 +15,73 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { if (selection == null) { return KeyEventResult.ignored; } - final node = editorState.getNodeAtPath(selection.start.path); - final context = node?.context; - if (node == null || - context == null || - node.delta == null || - node.type == CodeBlockKeys.type) { + final context = editorState.getNodeAtPath(selection.start.path)?.context; + if (context == null) { return KeyEventResult.ignored; } + final container = Overlay.of(context); - emojiMenuService = EmojiMenu(editorState: editorState, overlay: container); - emojiMenuService?.show(''); + + Alignment alignment = Alignment.topLeft; + Offset offset = Offset.zero; + + final selectionService = editorState.service.selectionService; + final selectionRects = selectionService.selectionRects; + if (selectionRects.isEmpty) { + return KeyEventResult.ignored; + } + final rect = selectionRects.first; + + // Calculate the offset and alignment + // Don't like these values being hardcoded but unsure how to grab the + // values dynamically to match the /emoji command. + const menuHeight = 200.0; + const menuOffset = Offset(10, 10); // Tried (0, 10) but that looked off + + final editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorHeight = editorState.renderBox!.size.height; + final editorWidth = editorState.renderBox!.size.width; + + // show below default + alignment = Alignment.topLeft; + final bottomRight = rect.bottomRight; + final topRight = rect.topRight; + final newOffset = bottomRight + menuOffset; + offset = Offset( + newOffset.dx, + newOffset.dy, + ); + + // show above + if (newOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { + offset = topRight - menuOffset; + alignment = Alignment.bottomLeft; + + offset = Offset( + newOffset.dx, + MediaQuery.of(context).size.height - newOffset.dy, + ); + } + + // show on left + if (offset.dx - editorOffset.dx > editorWidth / 2) { + alignment = alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + offset = Offset( + editorWidth - offset.dx + editorOffset.dx, + offset.dy, + ); + } + + showEmojiPickerMenu( + container, + editorState, + alignment, + offset, + ); + return KeyEventResult.handled; }; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart deleted file mode 100644 index 6f143a83c1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class InviteMemberByLink extends StatelessWidget { - const InviteMemberByLink({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Title(), - _Description(), - ], - ), - Spacer(), - _CopyLinkButton(), - ], - ); - } -} - -class _Title extends StatelessWidget { - const _Title(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text( - LocaleKeys.settings_appearance_members_inviteLinkToAddMember.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ); - } -} - -class _Description extends StatelessWidget { - const _Description(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_appearance_members_clickToCopyLink.tr(), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: ' ${LocaleKeys.settings_appearance_members_or.tr()} ', - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: LocaleKeys.settings_appearance_members_generateANewLink.tr(), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.action, - ), - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer() - ..onTap = () => _onGenerateInviteLink(context), - ), - ], - ), - ); - } - - Future _onGenerateInviteLink(BuildContext context) async { - final inviteLink = context.read().state.inviteLink; - if (inviteLink != null) { - // show a dialog to confirm if the user wants to copy the link to the clipboard - await showConfirmDialog( - context: context, - style: ConfirmPopupStyle.cancelAndOk, - title: 'Reset the invite link?', - description: - 'Resetting will deactivate the current link for all space members and generate a new one. The old link will no longer be available.', - confirmLabel: 'Reset', - onConfirm: () { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - }, - confirmButtonBuilder: (_) => AFFilledTextButton.destructive( - text: 'Reset', - onTap: () { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - - Navigator.of(context).pop(); - }, - ), - ); - } else { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - } - } -} - -class _CopyLinkButton extends StatelessWidget { - const _CopyLinkButton(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFOutlinedTextButton.normal( - text: LocaleKeys.button_copyLink.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.l, - vertical: theme.spacing.s, - ), - onTap: () { - final link = context.read().state.inviteLink; - if (link != null) { - getIt().setData( - ClipboardServiceData( - plainText: link, - ), - ); - - showToastNotification( - message: LocaleKeys.document_inlineLink_copyLink.tr(), - ); - } else { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkFailed.tr(), - ); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart deleted file mode 100644 index 9f8ce45a97..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; - -class InviteMemberByEmail extends StatefulWidget { - const InviteMemberByEmail({super.key}); - - @override - State createState() => _InviteMemberByEmailState(); -} - -class _InviteMemberByEmailState extends State { - final _emailController = TextEditingController(); - - @override - void dispose() { - _emailController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.settings_appearance_members_inviteMemberByEmail.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - VSpace(theme.spacing.m), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: AFTextField( - controller: _emailController, - hintText: - LocaleKeys.settings_appearance_members_inviteHint.tr(), - onSubmitted: (value) => _inviteMember(), - ), - ), - HSpace(theme.spacing.l), - AFFilledTextButton.primary( - text: LocaleKeys.settings_appearance_members_sendInvite.tr(), - onTap: _inviteMember, - ), - ], - ), - ], - ); - } - - void _inviteMember() { - final email = _emailController.text; - if (!isEmail(email)) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), - ); - return; - } - - context - .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); - // clear the email field after inviting - _emailController.clear(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart deleted file mode 100644 index 01d507ea24..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:http/http.dart' as http; - -enum InviteCodeEndpoint { - getInviteCode, - deleteInviteCode, - generateInviteCode; - - String get path { - switch (this) { - case InviteCodeEndpoint.getInviteCode: - case InviteCodeEndpoint.deleteInviteCode: - case InviteCodeEndpoint.generateInviteCode: - return '/api/workspace/{workspaceId}/invite-code'; - } - } - - String get method { - switch (this) { - case InviteCodeEndpoint.getInviteCode: - return 'GET'; - case InviteCodeEndpoint.deleteInviteCode: - return 'DELETE'; - case InviteCodeEndpoint.generateInviteCode: - return 'POST'; - } - } - - Uri uri(String baseUrl, String workspaceId) => - Uri.parse(path.replaceAll('{workspaceId}', workspaceId)).replace( - scheme: Uri.parse(baseUrl).scheme, - host: Uri.parse(baseUrl).host, - port: Uri.parse(baseUrl).port, - ); -} - -class MemberHttpService { - MemberHttpService({ - required this.baseUrl, - required this.authToken, - }); - - final String baseUrl; - final String authToken; - - final http.Client client = http.Client(); - - Map get headers => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $authToken', - }; - - /// Gets the invite code for a workspace - Future> getInviteCode({ - required String workspaceId, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.getInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to get invite code', - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['code'] as String), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to get invite code: $e'), - ); - } - } - - /// Deletes the invite code for a workspace - Future> deleteInviteCode({ - required String workspaceId, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.deleteInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to delete invite code', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Generates a new invite code for a workspace - /// - /// [workspaceId] - The ID of the workspace - Future> generateInviteCode({ - required String workspaceId, - int? validityPeriodHours, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.generateInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to generate invite code', - body: { - 'validity_period_hours': validityPeriodHours, - }, - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['data']['code'].toString()), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to generate invite code: $e'), - ); - } - } - - /// Makes a request to the specified endpoint - Future> _makeRequest({ - required InviteCodeEndpoint endpoint, - required String workspaceId, - Map? body, - String errorMessage = 'Request failed', - }) async { - try { - final uri = endpoint.uri(baseUrl, workspaceId); - http.Response response; - - switch (endpoint.method) { - case 'GET': - response = await client.get( - uri, - headers: headers, - ); - break; - case 'DELETE': - response = await client.delete( - uri, - headers: headers, - ); - break; - case 'POST': - response = await client.post( - uri, - headers: headers, - body: body != null ? jsonEncode(body) : null, - ); - break; - default: - return FlowyResult.failure( - FlowyError(msg: 'Invalid request method: ${endpoint.method}'), - ); - } - - if (response.statusCode == 200) { - if (response.body.isNotEmpty) { - return FlowyResult.success(jsonDecode(response.body)); - } - return FlowyResult.success(true); - } else { - final errorBody = - response.body.isNotEmpty ? jsonDecode(response.body) : {}; - - Log.info( - '${endpoint.name} request failed: ${response.statusCode}, $errorBody', - ); - - return FlowyResult.failure( - FlowyError( - msg: errorBody['msg'] ?? errorMessage, - ), - ); - } - } catch (e) { - Log.error('${endpoint.name} request failed: error: $e'); - - return FlowyResult.failure( - FlowyError(msg: 'Network error: ${e.toString()}'), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index 3fc13c7b18..c9fcb34204 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -1,11 +1,8 @@ import 'dart:async'; import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/shared/af_user_profile_extension.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -37,260 +34,163 @@ class WorkspaceMemberBloc super(WorkspaceMemberState.initial()) { on((event, emit) async { await event.when( - initial: () async => _onInitial(emit, workspaceId), - getWorkspaceMembers: () async => _onGetWorkspaceMembers(emit), - addWorkspaceMember: (email) async => _onAddWorkspaceMember(emit, email), - inviteWorkspaceMemberByEmail: (email) async => - _onInviteWorkspaceMemberByEmail(emit, email), - removeWorkspaceMemberByEmail: (email) async => - _onRemoveWorkspaceMemberByEmail(emit, email), - inviteWorkspaceMemberByLink: (link) async => - _onInviteWorkspaceMemberByLink(emit, link), - generateInviteLink: () async => _onGenerateInviteLink(emit), - updateWorkspaceMember: (email, role) async => - _onUpdateWorkspaceMember(emit, email, role), + initial: () async { + await _setCurrentWorkspaceId(workspaceId); + + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + + if (myRole.isOwner) { + unawaited(_fetchWorkspaceSubscriptionInfo()); + } + emit( + state.copyWith( + members: members, + myRole: myRole, + isLoading: false, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + }, + getWorkspaceMembers: () async { + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + }, + addWorkspaceMember: (email) async { + final result = await _userBackendService.addWorkspaceMember( + _workspaceId, + email, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.add, + result: result, + ), + ), + ); + // the addWorkspaceMember doesn't return the updated members, + // so we need to get the members again + result.onSuccess((s) { + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }); + }, + inviteWorkspaceMember: (email) async { + final result = await _userBackendService.inviteWorkspaceMember( + _workspaceId, + email, + role: AFRolePB.Member, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.invite, + result: result, + ), + ), + ); + }, + removeWorkspaceMember: (email) async { + final result = await _userBackendService.removeWorkspaceMember( + _workspaceId, + email, + ); + final members = result.fold( + (s) => state.members.where((e) => e.email != email).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.remove, + result: result, + ), + ), + ); + }, + updateWorkspaceMember: (email, role) async { + final result = await _userBackendService.updateWorkspaceMember( + _workspaceId, + email, + role, + ); + final members = result.fold( + (s) => state.members.map((e) { + if (e.email == email) { + e.freeze(); + return e.rebuild((p0) => p0.role = role); + } + return e; + }).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.updateRole, + result: result, + ), + ), + ); + }, updateSubscriptionInfo: (info) async => - _onUpdateSubscriptionInfo(emit, info), - upgradePlan: () async => _onUpgradePlan(), + emit(state.copyWith(subscriptionInfo: info)), + upgradePlan: () async { + final plan = state.subscriptionInfo?.plan; + if (plan == null) { + return Log.error('Failed to upgrade plan: plan is null'); + } + + if (plan == WorkspacePlanPB.FreePlan) { + final checkoutLink = await _userBackendService.createSubscription( + _workspaceId, + SubscriptionPlanPB.Pro, + ); + + checkoutLink.fold( + (pl) => afLaunchUrlString(pl.paymentLink), + (f) => Log.error('Failed to create subscription: ${f.msg}', f), + ); + } + }, ); }); } final UserProfilePB userProfile; + + // if the workspace is null, use the current workspace final UserWorkspacePB? workspace; + late final String _workspaceId; final UserBackendService _userBackendService; - MemberHttpService? _memberHttpService; - - Future _onInitial( - Emitter emit, - String? workspaceId, - ) async { - await _setCurrentWorkspaceId(workspaceId); - - final result = await _userBackendService.getWorkspaceMembers(_workspaceId); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - - if (myRole.isOwner) { - unawaited(_fetchWorkspaceSubscriptionInfo()); - } - - final baseUrl = await getAppFlowyCloudUrl(); - final authToken = userProfile.authToken; - if (authToken != null) { - _memberHttpService = MemberHttpService( - baseUrl: baseUrl, - authToken: authToken, - ); - unawaited( - _memberHttpService?.getInviteCode(workspaceId: _workspaceId).fold( - (s) async { - final inviteLink = await _buildInviteLink(inviteCode: s); - emit(state.copyWith(inviteLink: inviteLink)); - }, - (e) => Log.info('Failed to get invite code: ${e.msg}', e), - ), - ); - } else { - Log.error('Failed to get auth token'); - } - - emit( - state.copyWith( - members: members, - myRole: myRole, - isLoading: false, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - } - - Future _onGetWorkspaceMembers( - Emitter emit, - ) async { - final result = await _userBackendService.getWorkspaceMembers(_workspaceId); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - emit( - state.copyWith( - members: members, - myRole: myRole, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - } - - Future _onAddWorkspaceMember( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.addWorkspaceMember( - _workspaceId, - email, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.addByEmail, - result: result, - ), - ), - ); - // the addWorkspaceMember doesn't return the updated members, - // so we need to get the members again - result.onSuccess((s) { - add(const WorkspaceMemberEvent.getWorkspaceMembers()); - }); - } - - Future _onInviteWorkspaceMemberByEmail( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.inviteWorkspaceMember( - _workspaceId, - email, - role: AFRolePB.Member, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.inviteByEmail, - result: result, - ), - ), - ); - } - - Future _onRemoveWorkspaceMemberByEmail( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.removeWorkspaceMember( - _workspaceId, - email, - ); - final members = result.fold( - (s) => state.members.where((e) => e.email != email).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.removeByEmail, - result: result, - ), - ), - ); - } - - Future _onInviteWorkspaceMemberByLink( - Emitter emit, - String link, - ) async {} - - Future _onGenerateInviteLink(Emitter emit) async { - final result = await _memberHttpService?.generateInviteCode( - workspaceId: _workspaceId, - ); - - await result?.fold( - (s) async { - final inviteLink = await _buildInviteLink(inviteCode: s); - emit( - state.copyWith( - inviteLink: inviteLink, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.generateInviteLink, - result: result, - ), - ), - ); - }, - (e) async { - Log.error('Failed to generate invite link: ${e.msg}', e); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.generateInviteLink, - result: result, - ), - ), - ); - }, - ); - } - - Future _onUpdateWorkspaceMember( - Emitter emit, - String email, - AFRolePB role, - ) async { - final result = await _userBackendService.updateWorkspaceMember( - _workspaceId, - email, - role, - ); - final members = result.fold( - (s) => state.members.map((e) { - if (e.email == email) { - e.freeze(); - return e.rebuild((p0) => p0.role = role); - } - return e; - }).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.updateRole, - result: result, - ), - ), - ); - } - - Future _onUpdateSubscriptionInfo( - Emitter emit, - WorkspaceSubscriptionInfoPB info, - ) async { - emit(state.copyWith(subscriptionInfo: info)); - } - - Future _onUpgradePlan() async { - final plan = state.subscriptionInfo?.plan; - if (plan == null) { - return Log.error('Failed to upgrade plan: plan is null'); - } - - if (plan == WorkspacePlanPB.FreePlan) { - final checkoutLink = await _userBackendService.createSubscription( - _workspaceId, - SubscriptionPlanPB.Pro, - ); - - checkoutLink.fold( - (pl) => afLaunchUrlString(pl.paymentLink), - (f) => Log.error('Failed to create subscription: ${f.msg}', f), - ); - } - } AFRolePB _getMyRole(List members) { final role = members @@ -322,6 +222,8 @@ class WorkspaceMemberBloc } } + // We fetch workspace subscription info lazily as it's not needed in the first + // render of the page. Future _fetchWorkspaceSubscriptionInfo() async { final result = await UserBackendService.getWorkspaceSubscriptionInfo(_workspaceId); @@ -335,15 +237,6 @@ class WorkspaceMemberBloc (f) => Log.error('Failed to fetch subscription info: ${f.msg}', f), ); } - - Future _buildInviteLink({required String inviteCode}) async { - final baseUrl = await getAppFlowyShareDomain(); - final authToken = userProfile.authToken; - if (authToken != null) { - return '$baseUrl/app/invited/$inviteCode'; - } - return ''; - } } @freezed @@ -353,15 +246,10 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent { GetWorkspaceMembers; const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = AddWorkspaceMember; - const factory WorkspaceMemberEvent.inviteWorkspaceMemberByEmail( - String email, - ) = InviteWorkspaceMemberByEmail; - const factory WorkspaceMemberEvent.removeWorkspaceMemberByEmail( - String email, - ) = RemoveWorkspaceMemberByEmail; - const factory WorkspaceMemberEvent.inviteWorkspaceMemberByLink(String link) = - InviteWorkspaceMemberByLink; - const factory WorkspaceMemberEvent.generateInviteLink() = GenerateInviteLink; + const factory WorkspaceMemberEvent.inviteWorkspaceMember(String email) = + InviteWorkspaceMember; + const factory WorkspaceMemberEvent.removeWorkspaceMember(String email) = + RemoveWorkspaceMember; const factory WorkspaceMemberEvent.updateWorkspaceMember( String email, AFRolePB role, @@ -377,12 +265,10 @@ enum WorkspaceMemberActionType { none, get, // this event will send an invitation to the member - inviteByEmail, - inviteByLink, - generateInviteLink, + invite, // this event will add the member without sending an invitation - addByEmail, - removeByEmail, + add, + remove, updateRole, } @@ -406,7 +292,6 @@ class WorkspaceMemberState with _$WorkspaceMemberState { @Default(null) WorkspaceMemberActionResult? actionResult, @Default(true) bool isLoading, @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo, - @Default(null) String? inviteLink, }) = _WorkspaceMemberState; factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); @@ -422,7 +307,6 @@ class WorkspaceMemberState with _$WorkspaceMemberState { other.members == members && other.myRole == myRole && other.subscriptionInfo == subscriptionInfo && - other.inviteLink == inviteLink && identical(other.actionResult, actionResult); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index 3ead104ee3..bf33ab9d72 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,23 +1,22 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; class WorkspaceMembersPage extends StatelessWidget { const WorkspaceMembersPage({ @@ -39,14 +38,14 @@ class WorkspaceMembersPage extends StatelessWidget { builder: (context, state) { return SettingsBody( title: LocaleKeys.settings_appearance_members_title.tr(), - // Enable it when the backend support admin panel - // descriptionBuilder: _buildDescription, autoSeparate: false, children: [ + if (state.actionResult != null) ...[ + _showMemberLimitWarning(context, state), + const VSpace(16), + ], if (state.myRole.canInvite) ...[ - const InviteMemberByLink(), - const SettingsCategorySpacer(), - const InviteMemberByEmail(), + const _InviteMember(), const SettingsCategorySpacer(), ], if (state.members.isNotEmpty) @@ -62,141 +61,104 @@ class WorkspaceMembersPage extends StatelessWidget { ); } - // Enable it when the backend support admin panel - // Widget _buildDescription(BuildContext context) { - // final theme = AppFlowyTheme.of(context); - // return Text.rich( - // TextSpan( - // children: [ - // TextSpan( - // text: - // '${LocaleKeys.settings_appearance_members_memberPageDescription1.tr()} ', - // style: theme.textStyle.caption.standard( - // color: theme.textColorScheme.secondary, - // ), - // ), - // TextSpan( - // text: LocaleKeys.settings_appearance_members_adminPanel.tr(), - // style: theme.textStyle.caption.underline( - // color: theme.textColorScheme.secondary, - // ), - // mouseCursor: SystemMouseCursors.click, - // recognizer: TapGestureRecognizer() - // ..onTap = () async { - // final baseUrl = await getAppFlowyCloudUrl(); - // await afLaunchUrlString(baseUrl); - // }, - // ), - // TextSpan( - // text: - // ' ${LocaleKeys.settings_appearance_members_memberPageDescription2.tr()} ', - // style: theme.textStyle.caption.standard( - // color: theme.textColorScheme.secondary, - // ), - // ), - // ], - // ), - // ); - // } + Widget _showMemberLimitWarning( + BuildContext context, + WorkspaceMemberState state, + ) { + // We promise that state.actionResult != null before calling + // this method + final actionResult = state.actionResult!.result; + final actionType = state.actionResult!.actionType; - // Widget _showMemberLimitWarning( - // BuildContext context, - // WorkspaceMemberState state, - // ) { - // // We promise that state.actionResult != null before calling - // // this method - // final actionResult = state.actionResult!.result; - // final actionType = state.actionResult!.actionType; + if (actionType == WorkspaceMemberActionType.invite && + actionResult.isFailure) { + final error = actionResult.getFailure().code; + if (error == ErrorCode.WorkspaceMemberLimitExceeded) { + return Row( + children: [ + const FlowySvg( + FlowySvgs.warning_s, + blendMode: BlendMode.dst, + size: Size.square(20), + ), + const HSpace(12), + Expanded( + child: RichText( + text: TextSpan( + children: [ + if (state.subscriptionInfo?.plan == + WorkspacePlanPB.ProPlan) ...[ + TextSpan( + text: LocaleKeys + .settings_appearance_members_memberLimitExceededPro + .tr(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AFThemeExtension.of(context).strongText, + ), + ), + WidgetSpan( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + // Hardcoded support email, in the future we might + // want to add this to an environment variable + onTap: () async => afLaunchUrlString( + 'mailto:support@appflowy.io', + ), + child: FlowyText( + LocaleKeys + .settings_appearance_members_memberLimitExceededProContact + .tr(), + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ] else ...[ + TextSpan( + text: LocaleKeys + .settings_appearance_members_memberLimitExceeded + .tr(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AFThemeExtension.of(context).strongText, + ), + ), + WidgetSpan( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => context + .read() + .add(const WorkspaceMemberEvent.upgradePlan()), + child: FlowyText( + LocaleKeys + .settings_appearance_members_memberLimitExceededUpgrade + .tr(), + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ], + ], + ), + ), + ), + ], + ); + } + } - // if (actionType == WorkspaceMemberActionType.inviteByEmail && - // actionResult.isFailure) { - // final error = actionResult.getFailure().code; - // if (error == ErrorCode.WorkspaceMemberLimitExceeded) { - // return Row( - // children: [ - // const FlowySvg( - // FlowySvgs.warning_s, - // blendMode: BlendMode.dst, - // size: Size.square(20), - // ), - // const HSpace(12), - // Expanded( - // child: RichText( - // text: TextSpan( - // children: [ - // if (state.subscriptionInfo?.plan == - // WorkspacePlanPB.ProPlan) ...[ - // TextSpan( - // text: LocaleKeys - // .settings_appearance_members_memberLimitExceededPro - // .tr(), - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: AFThemeExtension.of(context).strongText, - // ), - // ), - // WidgetSpan( - // child: MouseRegion( - // cursor: SystemMouseCursors.click, - // child: GestureDetector( - // // Hardcoded support email, in the future we might - // // want to add this to an environment variable - // onTap: () async => afLaunchUrlString( - // 'mailto:support@appflowy.io', - // ), - // child: FlowyText( - // LocaleKeys - // .settings_appearance_members_memberLimitExceededProContact - // .tr(), - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // ), - // ] else ...[ - // TextSpan( - // text: LocaleKeys - // .settings_appearance_members_memberLimitExceeded - // .tr(), - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: AFThemeExtension.of(context).strongText, - // ), - // ), - // WidgetSpan( - // child: MouseRegion( - // cursor: SystemMouseCursors.click, - // child: GestureDetector( - // onTap: () => context - // .read() - // .add(const WorkspaceMemberEvent.upgradePlan()), - // child: FlowyText( - // LocaleKeys - // .settings_appearance_members_memberLimitExceededUpgrade - // .tr(), - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // ), - // ], - // ], - // ), - // ), - // ), - // ], - // ); - // } - // } - - // return const SizedBox.shrink(); - // } + return const SizedBox.shrink(); + } void _showResultDialog(BuildContext context, WorkspaceMemberState state) { final actionResult = state.actionResult; @@ -208,12 +170,12 @@ class WorkspaceMembersPage extends StatelessWidget { final result = actionResult.result; // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.addByEmail) { + if (actionType == WorkspaceMemberActionType.add) { result.fold( (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), ); }, (f) { @@ -227,12 +189,12 @@ class WorkspaceMembersPage extends StatelessWidget { ); }, ); - } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { + } else if (actionType == WorkspaceMemberActionType.invite) { result.fold( (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), ); }, (f) { @@ -252,27 +214,116 @@ class WorkspaceMembersPage extends StatelessWidget { ); }, ); - } else if (actionType == WorkspaceMemberActionType.generateInviteLink) { - result.fold( - (s) { - showToastNotification( - message: 'Invite link generated successfully', - ); + } + } +} - // copy the invite link to the clipboard - final inviteLink = state.inviteLink; - if (inviteLink != null) { - getIt().setPlainText(inviteLink); - } - }, - (f) { - Log.error('generate invite link failed: $f'); - showToastNotification( - message: 'Failed to generate invite link', - ); - }, +class _InviteMember extends StatefulWidget { + const _InviteMember(); + + @override + State<_InviteMember> createState() => _InviteMemberState(); +} + +class _InviteMemberState extends State<_InviteMember> { + final _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + LocaleKeys.settings_appearance_members_inviteMembers.tr(), + fontSize: 16.0, + ), + const VSpace(8.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 48.0, + ), + child: FlowyTextField( + hintText: + LocaleKeys.settings_appearance_members_inviteHint.tr(), + controller: _emailController, + onEditingComplete: _inviteMember, + ), + ), + ), + const HSpace(10.0), + SizedBox( + height: 48.0, + child: IntrinsicWidth( + child: PrimaryRoundedButton( + text: LocaleKeys.settings_appearance_members_sendInvite.tr(), + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + onTap: _inviteMember, + ), + ), + ), + ], + ), + /* Enable this when the feature is ready + PrimaryButton( + backgroundColor: const Color(0xFFE0E0E0), + child: Padding( + padding: const EdgeInsets.only( + left: 20, + right: 24, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.invite_member_link_m, + color: Colors.black, + ), + const HSpace(8.0), + FlowyText( + LocaleKeys.settings_appearance_members_copyInviteLink.tr(), + color: Colors.black, + ), + ], + ), + ), + onPressed: () { + showSnackBarMessage(context, 'not implemented'); + }, + ), + const VSpace(16.0), + */ + ], + ); + } + + void _inviteMember() { + final email = _emailController.text; + if (!isEmail(email)) { + return showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); } + context + .read() + .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); + // clear the email field after inviting + _emailController.clear(); } } @@ -289,12 +340,9 @@ class _MemberList extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => Divider( - color: theme.borderColorScheme.primary, - ), + separatorBuilder: () => const Divider(), children: [ const _MemberListHeader(), ...members.map( @@ -314,34 +362,31 @@ class _MemberListHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - LocaleKeys.settings_appearance_members_user.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), + FlowyText.semibold( + LocaleKeys.settings_appearance_members_label.tr(), + fontSize: 16.0, ), - Expanded( - child: Text( - LocaleKeys.settings_appearance_members_role.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, + const VSpace(16.0), + Row( + children: [ + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_user.tr(), + fontSize: 14.0, + ), ), - ), - ), - Expanded( - child: Text( - LocaleKeys.settings_accountPage_email_title.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_role.tr(), + fontSize: 14.0, + ), ), - ), + const HSpace(28.0), + ], ), - const HSpace(28.0), ], ); } @@ -360,42 +405,27 @@ class _MemberItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); + final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; return Row( children: [ Expanded( - child: Text( + child: FlowyText.medium( member.name, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), + color: textColor, + fontSize: 14.0, ), ), Expanded( child: member.role.isOwner || !myRole.canUpdate - ? Text( + ? FlowyText.medium( member.role.description, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), + color: textColor, + fontSize: 14.0, ) : _MemberRoleActionList( member: member, ), ), - Expanded( - child: FlowyTooltip( - message: member.email, - child: Text( - member.email, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - ), - ), - ), myRole.canDelete && member.email != userProfile.email // can't delete self ? _MemberMoreActionList(member: member) @@ -446,7 +476,7 @@ class _MemberMoreActionList extends StatelessWidget { .settings_appearance_members_areYouSureToRemoveMember .tr(), onOkPressed: () => context.read().add( - WorkspaceMemberEvent.removeWorkspaceMemberByEmail( + WorkspaceMemberEvent.removeWorkspaceMember( action.member.email, ), ), @@ -485,12 +515,106 @@ class _MemberRoleActionList extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text( - member.role.description, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), + return PopoverActionList<_MemberRoleActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithLeftAligned, + actions: [AFRolePB.Member] + .map((e) => _MemberRoleActionWrapper(e, member)) + .toList(), + offset: const Offset(0, 10), + buildChild: (controller) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => controller.show(), + child: Row( + children: [ + FlowyText.medium( + member.role.description, + fontSize: 14.0, + ), + const HSpace(8.0), + const FlowySvg( + FlowySvgs.drop_menu_show_s, + ), + ], + ), + ), + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case AFRolePB.Member: + case AFRolePB.Guest: + context.read().add( + WorkspaceMemberEvent.updateWorkspaceMember( + action.member.email, + action.inner, + ), + ); + break; + case AFRolePB.Owner: + break; + } + controller.close(); + }, ); } } + +class _MemberRoleActionWrapper extends ActionCell { + _MemberRoleActionWrapper(this.inner, this.member); + + final AFRolePB inner; + final WorkspaceMemberPB member; + + @override + Widget? rightIcon(Color iconColor) { + return SizedBox( + width: 58.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTooltip( + message: tooltip, + child: const FlowySvg( + FlowySvgs.information_s, + // color: iconColor, + ), + ), + const Spacer(), + if (member.role == inner) + const FlowySvg( + FlowySvgs.checkmark_tiny_s, + ), + ], + ), + ); + } + + @override + String get name { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guest.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_member.tr(); + case AFRolePB.Owner: + return LocaleKeys.settings_appearance_members_owner.tr(); + } + throw UnimplementedError('Unknown role: $inner'); + } + + String get tooltip { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guestHintText.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_memberHintText.tr(); + case AFRolePB.Owner: + return ''; + } + throw UnimplementedError('Unknown role: $inner'); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 5f158f4ae1..645c3daa65 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -3,7 +3,6 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; @@ -20,6 +19,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -71,7 +71,7 @@ class AppFlowyCloudViewSetting extends StatelessWidget { const VSpace(8), const AppFlowyCloudEnableSync(), const VSpace(6), - // const AppFlowyCloudSyncLogEnabled(), + const AppFlowyCloudSyncLogEnabled(), const VSpace(12), RestartButton( onClick: () { @@ -130,7 +130,7 @@ class CustomAppFlowyCloudView extends StatelessWidget { final List children = []; children.addAll([ const AppFlowyCloudEnableSync(), - // const AppFlowyCloudSyncLogEnabled(), + const AppFlowyCloudSyncLogEnabled(), const VSpace(40), ]); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index 8a85377efe..cf51d7a3e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -2,6 +2,7 @@ import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -63,8 +64,12 @@ class SettingThirdPartyLogin extends StatelessWidget { ) async { result.fold( (user) async { - didLogin(); - await runAppFlowy(); + if (user.encryptionType == EncryptionTypePB.Symmetric) { + getIt().pushEncryptionScreen(context, user); + } else { + didLogin(); + await runAppFlowy(); + } }, (error) => showSnapBar(context, error.msg), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index f628aadc6b..c9069b8be3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -52,51 +52,52 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.account, selectedPage: currentPage, label: LocaleKeys.settings_accountPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_user_m), + icon: const FlowySvg(FlowySvgs.settings_account_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.workspace, selectedPage: currentPage, label: LocaleKeys.settings_workspacePage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_workspace_m), + icon: const FlowySvg(FlowySvgs.settings_workplace_m), changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.membersSettings.isOn && - userProfile.workspaceAuthType == AuthTypePB.Server) + userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.member, selectedPage: currentPage, label: LocaleKeys.settings_appearance_members_label.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_users_m), + icon: const Icon(Icons.people), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.manageData, selectedPage: currentPage, label: LocaleKeys.settings_manageDataPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_database_m), + icon: const FlowySvg(FlowySvgs.settings_data_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.notifications, selectedPage: currentPage, label: LocaleKeys.settings_menu_notifications.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_bell_m), + icon: const Icon(Icons.notifications_outlined), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.cloud, selectedPage: currentPage, label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_cloud_m), + icon: const Icon(Icons.sync), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.shortcuts, selectedPage: currentPage, label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_keyboard_m), + icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( @@ -104,17 +105,18 @@ class SettingsMenu extends StatelessWidget { selectedPage: currentPage, label: LocaleKeys.settings_aiPage_menuLabel.tr(), icon: const FlowySvg( - FlowySvgs.settings_page_ai_m, + FlowySvgs.ai_summary_generate_s, size: Size.square(24), ), changeSelectedPage: changeSelectedPage, ), - if (userProfile.workspaceAuthType == AuthTypePB.Server) + if (userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) SettingsMenuElement( page: SettingsPage.sites, selectedPage: currentPage, label: LocaleKeys.settings_sites_title.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_earth_m), + icon: const Icon(Icons.web), changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[ @@ -122,15 +124,14 @@ class SettingsMenu extends StatelessWidget { page: SettingsPage.plan, selectedPage: currentPage, label: LocaleKeys.settings_planPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_plan_m), + icon: const FlowySvg(FlowySvgs.settings_plan_m), changeSelectedPage: changeSelectedPage, ), SettingsMenuElement( page: SettingsPage.billing, selectedPage: currentPage, label: LocaleKeys.settings_billingPage_menuLabel.tr(), - icon: - const FlowySvg(FlowySvgs.settings_page_credit_card_m), + icon: const FlowySvg(FlowySvgs.settings_billing_m), changeSelectedPage: changeSelectedPage, ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index bdc5ef0546..9e25dece0e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -1,12 +1,12 @@ import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart index d965670f77..8bfc187422 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart @@ -23,7 +23,6 @@ abstract class AppFlowyDatePicker extends StatefulWidget { this.onIncludeTimeChanged, this.onIsRangeChanged, this.onReminderSelected, - this.enableDidUpdate = true, }); final DateTime? dateTime; @@ -56,7 +55,6 @@ abstract class AppFlowyDatePicker extends StatefulWidget { final ReminderOption reminderOption; final OnReminderSelected? onReminderSelected; - final bool enableDidUpdate; } abstract class AppFlowyDatePickerState @@ -77,29 +75,32 @@ abstract class AppFlowyDatePickerState @override void initState() { super.initState(); - initData(); - focusedDateTime = widget.dateTime ?? DateTime.now(); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (widget.enableDidUpdate) { - initData(); - } - if (oldWidget.reminderOption != widget.reminderOption) { - reminderOption = widget.reminderOption; - } - super.didUpdateWidget(oldWidget); - } - - void initData() { dateTime = widget.dateTime; startDateTime = widget.isRange ? widget.dateTime : null; endDateTime = widget.isRange ? widget.endDateTime : null; includeTime = widget.includeTime; isRange = widget.isRange; reminderOption = widget.reminderOption; + + focusedDateTime = widget.dateTime ?? DateTime.now(); + } + + @override + void didUpdateWidget(covariant oldWidget) { + dateTime = widget.dateTime; + if (widget.isRange) { + startDateTime = widget.dateTime; + endDateTime = widget.endDateTime; + } else { + startDateTime = endDateTime = null; + } + includeTime = widget.includeTime; + isRange = widget.isRange; + if (oldWidget.reminderOption != widget.reminderOption) { + reminderOption = widget.reminderOption; + } + super.didUpdateWidget(oldWidget); } void onDateSelectedFromDatePicker( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart index fada23e994..c404f576b1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart @@ -32,7 +32,6 @@ class DesktopAppFlowyDatePicker extends AppFlowyDatePicker { super.onIncludeTimeChanged, super.onIsRangeChanged, super.onReminderSelected, - super.enableDidUpdate, this.popoverMutex, this.options = const [], }); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index 54fc2fac2a..301fd038ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; class DatePickerOptions { DatePickerOptions({ DateTime? focusedDay, + this.popoverMutex, this.selectedDay, this.includeTime = false, this.isRange = false, @@ -30,6 +31,7 @@ class DatePickerOptions { }) : focusedDay = focusedDay ?? DateTime.now(); final DateTime focusedDay; + final PopoverMutex? popoverMutex; final DateTime? selectedDay; final bool includeTime; final bool isRange; @@ -46,7 +48,6 @@ class DatePickerOptions { abstract class DatePickerService { void show(Offset offset, {required DatePickerOptions options}); - void dismiss(); } @@ -59,7 +60,6 @@ class DatePickerMenu extends DatePickerService { final BuildContext context; final EditorState editorState; - PopoverMutex? popoverMutex; OverlayEntry? _menuEntry; @@ -67,9 +67,6 @@ class DatePickerMenu extends DatePickerService { void dismiss() { _menuEntry?.remove(); _menuEntry = null; - popoverMutex?.close(); - popoverMutex?.dispose(); - popoverMutex = null; } @override @@ -100,7 +97,6 @@ class DatePickerMenu extends DatePickerService { } } - popoverMutex = PopoverMutex(); _menuEntry = OverlayEntry( builder: (_) => Material( type: MaterialType.transparency, @@ -123,7 +119,6 @@ class DatePickerMenu extends DatePickerService { offset: Offset(offsetX, offsetY), showBelow: showBelow, options: options, - popoverMutex: popoverMutex, ), ], ), @@ -142,13 +137,11 @@ class _AnimatedDatePicker extends StatelessWidget { required this.offset, required this.showBelow, required this.options, - this.popoverMutex, }); final Offset offset; final bool showBelow; final DatePickerOptions options; - final PopoverMutex? popoverMutex; @override Widget build(BuildContext context) { @@ -172,12 +165,11 @@ class _AnimatedDatePicker extends StatelessWidget { dateFormat: options.dateFormat.simplified, timeFormat: options.timeFormat.simplified, dateTime: options.selectedDay, - popoverMutex: popoverMutex, + popoverMutex: options.popoverMutex, reminderOption: options.selectedReminderOption ?? ReminderOption.none, onDaySelected: options.onDaySelected, onRangeSelected: options.onRangeSelected, onReminderSelected: options.onReminderSelected, - enableDidUpdate: false, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart deleted file mode 100644 index 43ab8897e1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -typedef SimpleAFDialogAction = (String, void Function(BuildContext)?); - -/// A simple dialog with a title, content, and actions. -/// -/// The primary button is a filled button and colored using theme or destructive -/// color depending on the [isDestructive] parameter. The secondary button is an -/// outlined button. -/// -Future showSimpleAFDialog({ - required BuildContext context, - required String title, - required String content, - bool isDestructive = false, - required SimpleAFDialogAction primaryAction, - SimpleAFDialogAction? secondaryAction, - bool barrierDismissible = true, -}) { - final theme = AppFlowyTheme.of(context); - - return showDialog( - context: context, - barrierColor: theme.surfaceColorScheme.overlay, - barrierDismissible: barrierDismissible, - builder: (_) { - return AFModal( - constraints: BoxConstraints( - maxWidth: AFModalDimension.S, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AFModalHeader( - leading: Text( - title, - style: theme.textStyle.heading4.standard( - color: theme.textColorScheme.primary, - ), - ), - trailing: [ - AFGhostButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.close_s, - size: Size.square(20), - ); - }, - ), - ], - ), - Flexible( - child: ConstrainedBox( - // AFModalDimension.dialogHeight - header - footer - constraints: BoxConstraints(minHeight: 108.0), - child: AFModalBody( - child: Text(content), - ), - ), - ), - AFModalFooter( - trailing: [ - if (secondaryAction != null) - AFOutlinedButton.normal( - onTap: () { - secondaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text(secondaryAction.$1); - }, - ), - isDestructive - ? AFFilledButton.destructive( - onTap: () { - primaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text( - primaryAction.$1, - style: TextStyle( - color: AppFlowyTheme.of(context) - .textColorScheme - .onFill, - ), - ); - }, - ) - : AFFilledButton.primary( - onTap: () { - primaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text(primaryAction.$1); - }, - ), - ], - ), - ], - ), - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 8d65ee23bb..aa541b902c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -157,6 +157,7 @@ class _NavigatorTextFieldDialogState extends State { onOkPressed: () { if (newValue.isEmpty) { showToastNotification( + context, message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), ); return; @@ -362,7 +363,8 @@ class OkCancelButton extends StatelessWidget { } } -ToastificationItem showToastNotification({ +void showToastNotification( + BuildContext context, { String? message, TextSpan? richMessage, String? description, @@ -374,7 +376,7 @@ ToastificationItem showToastNotification({ (message == null) != (richMessage == null), "Exactly one of message or richMessage must be non-null.", ); - return toastification.showCustom( + toastification.showCustom( alignment: Alignment.bottomCenter, autoCloseDuration: const Duration(milliseconds: 3000), callbacks: callbacks ?? const ToastificationCallbacks(), @@ -606,7 +608,6 @@ Future showConfirmDialog({ VoidCallback? onCancel, String? confirmLabel, ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk, - WidgetBuilder? confirmButtonBuilder, }) { return showDialog( context: context, @@ -620,7 +621,6 @@ Future showConfirmDialog({ child: ConfirmPopup( title: title, description: description, - confirmButtonBuilder: confirmButtonBuilder, onConfirm: () => onConfirm?.call(), onCancel: () => onCancel?.call(), confirmLabel: confirmLabel, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index e3117c7f86..d666e606f6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -86,7 +86,7 @@ class _BubbleActionListState extends State { ), buildChild: (controller) { return FlowyTooltip( - message: LocaleKeys.questionBubble_getSupport.tr(), + message: LocaleKeys.questionBubble_help.tr(), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( @@ -121,22 +121,22 @@ class _BubbleActionListState extends State { if (action is BubbleActionWrapper) { switch (action.inner) { case BubbleAction.whatsNews: - afLaunchUrlString('https://www.appflowy.io/what-is-new'); + afLaunchUrlString("https://www.appflowy.io/what-is-new"); break; - case BubbleAction.getSupport: - afLaunchUrlString('https://discord.gg/9Q2xaN37tV'); + case BubbleAction.help: + afLaunchUrlString("https://discord.gg/9Q2xaN37tV"); break; case BubbleAction.debug: _DebugToast().show(); break; case BubbleAction.shortcuts: afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/shortcuts', + "https://docs.appflowy.io/docs/appflowy/product/shortcuts", ); break; case BubbleAction.markdown: afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/markdown', + "https://docs.appflowy.io/docs/appflowy/product/markdown", ); break; case BubbleAction.github: @@ -144,11 +144,6 @@ class _BubbleActionListState extends State { 'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose', ); break; - case BubbleAction.helpAndDocumentation: - afLaunchUrlString( - 'https://appflowy.com/guide', - ); - break; } } @@ -160,7 +155,7 @@ class _BubbleActionListState extends State { class _DebugToast { void show() async { - String debugInfo = ''; + String debugInfo = ""; debugInfo += await _getDeviceInfo(); debugInfo += await _getDocumentPath(); await Clipboard.setData(ClipboardData(text: debugInfo)); @@ -173,21 +168,20 @@ class _DebugToast { final deviceInfo = await deviceInfoPlugin.deviceInfo; return deviceInfo.data.entries - .fold('', (prev, el) => '$prev${el.key}: ${el.value}\n'); + .fold('', (prev, el) => "$prev${el.key}: ${el.value}\n"); } Future _getDocumentPath() async { return appFlowyApplicationDataDirectory().then((directory) { final path = directory.path.toString(); - return 'Document: $path\n'; + return "Document: $path\n"; }); } } enum BubbleAction { whatsNews, - helpAndDocumentation, - getSupport, + help, debug, shortcuts, markdown, @@ -210,10 +204,8 @@ extension QuestionBubbleExtension on BubbleAction { switch (this) { case BubbleAction.whatsNews: return LocaleKeys.questionBubble_whatsNew.tr(); - case BubbleAction.helpAndDocumentation: - return LocaleKeys.questionBubble_helpAndDocumentation.tr(); - case BubbleAction.getSupport: - return LocaleKeys.questionBubble_getSupport.tr(); + case BubbleAction.help: + return LocaleKeys.questionBubble_help.tr(); case BubbleAction.debug: return LocaleKeys.questionBubble_debug_name.tr(); case BubbleAction.shortcuts: @@ -229,12 +221,7 @@ extension QuestionBubbleExtension on BubbleAction { switch (this) { case BubbleAction.whatsNews: return const FlowySvg(FlowySvgs.star_s); - case BubbleAction.helpAndDocumentation: - return const FlowySvg( - FlowySvgs.help_and_documentation_s, - size: Size.square(16.0), - ); - case BubbleAction.getSupport: + case BubbleAction.help: return const FlowySvg(FlowySvgs.message_support_s); case BubbleAction.debug: return const FlowySvg(FlowySvgs.debug_s); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart index 8b58557455..43b3ab9b62 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart @@ -25,13 +25,18 @@ class SocialMediaSection extends CustomActionCell { action: SocialMediaWrapper(social), itemHeight: ActionListSizes.itemHeight, onSelected: (action) { - final url = switch (action.inner) { - SocialMedia.reddit => 'https://www.reddit.com/r/AppFlowy/', - SocialMedia.twitter => 'https://x.com/appflowy', - SocialMedia.forum => 'https://forum.appflowy.com/', - }; - - afLaunchUrlString(url); + switch (action.inner) { + case SocialMedia.reddit: + afLaunchUrlString( + 'https://www.reddit.com/r/AppFlowy/', + ); + case SocialMedia.twitter: + afLaunchUrlString( + 'https://x.com/appflowy', + ); + case SocialMedia.forum: + afLaunchUrlString('https://forum.appflowy.io/'); + } }, ); }, @@ -80,11 +85,11 @@ extension QuestionBubbleExtension on SocialMedia { String get name { switch (this) { case SocialMedia.forum: - return 'Community Forum'; + return "Community Forum"; case SocialMedia.twitter: - return 'Twitter – @appflowy'; + return "Twitter – @appflowy"; case SocialMedia.reddit: - return 'Reddit – r/appflowy'; + return "Reddit – r/appflowy"; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart index f6a2caa5a2..923f695188 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart @@ -51,6 +51,7 @@ class FlowyVersionSection extends CustomActionCell { } enableDocumentInternalLog = !enableDocumentInternalLog; showToastNotification( + context, message: enableDocumentInternalLog ? 'Enabled Internal Log' : 'Disabled Internal Log', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 62b3ccc8f3..90e47e7c19 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -96,7 +96,7 @@ class _MoreViewActionsState extends State { return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty && - userProfile.workspaceAuthType == AuthTypePB.Server) { + userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart index 2ecec3244c..10bc3dde34 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart @@ -10,8 +10,10 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -105,8 +107,6 @@ class CustomViewAction extends StatelessWidget { required this.view, required this.leftIcon, required this.label, - this.tooltipMessage, - this.disabled = false, this.onTap, this.mutex, }); @@ -114,8 +114,6 @@ class CustomViewAction extends StatelessWidget { final ViewPB view; final FlowySvgData leftIcon; final String label; - final bool disabled; - final String? tooltipMessage; final VoidCallback? onTap; final PopoverMutex? mutex; @@ -124,23 +122,17 @@ class CustomViewAction extends StatelessWidget { return Container( height: 34, padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyTooltip( - message: tooltipMessage, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - disable: disabled, - onTap: onTap, - leftIcon: FlowySvg( - leftIcon, - size: const Size.square(16.0), - color: disabled ? Theme.of(context).disabledColor : null, - ), - iconPadding: 10.0, - text: FlowyText( - label, - figmaLineHeight: 18.0, - color: disabled ? Theme.of(context).disabledColor : null, - ), + child: FlowyIconTextButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + onTap: onTap, + leftIconBuilder: (onHover) => FlowySvg( + leftIcon, + size: const Size.square(16.0), + ), + iconPadding: 10.0, + textBuilder: (onHover) => FlowyText( + label, + figmaLineHeight: 18.0, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 3be0973123..04ae9b30ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -74,6 +74,7 @@ class ViewTitleBar extends StatelessWidget { listener: (context, state) { if (state.isLocked) { showToastNotification( + context, message: LocaleKeys.lockPage_pageLockedToast.tr(), ); } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index e06670c5a5..30ee626f09 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -150,7 +150,7 @@ SPEC CHECKSUMS: bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 + device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig index d2b3d7e9b3..da469610eb 100644 --- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = AppFlowy PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 AppFlowy.IO. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2024 AppFlowy.IO. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift index b3c1761412..d53ef64377 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift @@ -1,13 +1,9 @@ import Cocoa import FlutterMacOS -@main +@NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index f69fd16927..69e287f117 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -4,11 +4,11 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:logger/logger.dart'; import 'ffi.dart' as ffi; @@ -62,15 +62,28 @@ class RustLogStreamReceiver { late StreamController _streamController; late StreamSubscription _subscription; int get port => _ffiPort.sendPort.nativePort; + late Logger _logger; RustLogStreamReceiver._internal() { _ffiPort = RawReceivePort(); _streamController = StreamController(); _ffiPort.handler = _streamController.add; + _logger = Logger( + printer: PrettyPrinter( + methodCount: 0, // number of method calls to be displayed + errorMethodCount: 8, // number of method calls if stacktrace is provided + lineLength: 120, // width of the output + colors: false, // Colorful log messages + printEmojis: false, // Print an emoji for each log message + dateTimeFormat: + DateTimeFormat.none, // Should each log print contain a timestamp + ), + level: kDebugMode ? Level.trace : Level.info, + ); _subscription = _streamController.stream.listen((data) { String decodedString = utf8.decode(data); - Log.info(decodedString); + _logger.i(decodedString); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart index ce0a4e2248..355a196621 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart @@ -3,43 +3,64 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; -import 'package:talker/talker.dart'; +import 'package:logger/logger.dart'; import 'ffi.dart'; class Log { static final shared = Log(); + // ignore: unused_field + late Logger _logger; - late Talker _logger; - - bool enableFlutterLog = true; + bool _enabled = false; // used to disable log in tests @visibleForTesting bool disableLog = false; Log() { - _logger = Talker( - filter: LogLevelTalkerFilter(), + _logger = Logger( + printer: PrettyPrinter( + methodCount: 2, // Number of method calls to be displayed + errorMethodCount: 8, // Number of method calls if stacktrace is provided + lineLength: 120, // Width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + ), + level: kDebugMode ? Level.trace : Level.info, ); } + static void enableFlutterLog() { + shared._enabled = true; + } + // Generic internal logging function to reduce code duplication - static void _log( - LogLevel level, - int rustLevel, - dynamic msg, [ - dynamic error, - StackTrace? stackTrace, - ]) { - // only forward logs to flutter in debug mode, otherwise log to rust to - // persist logs in the file system - if (shared.enableFlutterLog && kDebugMode) { - shared._logger.log(msg, logLevel: level, stackTrace: stackTrace); - } else { - String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace); - rust_log(rustLevel, toNativeUtf8(formattedMessage)); + static void _log(Level level, int rustLevel, dynamic msg, + [dynamic error, StackTrace? stackTrace]) { + if (shared._enabled) { + switch (level) { + case Level.info: + shared._logger.i(msg, stackTrace: stackTrace); + break; + case Level.debug: + shared._logger.d(msg, stackTrace: stackTrace); + break; + case Level.warning: + shared._logger.w(msg, stackTrace: stackTrace); + break; + case Level.error: + shared._logger.e(msg, stackTrace: stackTrace); + break; + case Level.trace: + shared._logger.t(msg, stackTrace: stackTrace); + break; + default: + shared._logger.log(level, msg, stackTrace: stackTrace); + } } + String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace); + rust_log(rustLevel, toNativeUtf8(formattedMessage)); } static void info(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -47,7 +68,7 @@ class Log { return; } - _log(LogLevel.info, 0, msg, error, stackTrace); + _log(Level.info, 0, msg, error, stackTrace); } static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -55,7 +76,7 @@ class Log { return; } - _log(LogLevel.debug, 1, msg, error, stackTrace); + _log(Level.debug, 1, msg, error, stackTrace); } static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -63,7 +84,7 @@ class Log { return; } - _log(LogLevel.warning, 3, msg, error, stackTrace); + _log(Level.warning, 3, msg, error, stackTrace); } static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -71,7 +92,7 @@ class Log { return; } - _log(LogLevel.verbose, 2, msg, error, stackTrace); + _log(Level.trace, 2, msg, error, stackTrace); } static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) { @@ -79,7 +100,7 @@ class Log { return; } - _log(LogLevel.error, 4, msg, error, stackTrace); + _log(Level.error, 4, msg, error, stackTrace); } } @@ -98,11 +119,3 @@ String _formatMessageWithStackTrace(dynamic msg, StackTrace? stackTrace) { } return msg.toString(); } - -class LogLevelTalkerFilter implements TalkerFilter { - @override - bool filter(TalkerData data) { - // filter out the debug logs in release mode - return kDebugMode ? true : data.logLevel != LogLevel.debug; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml index 18aea4838b..9ff267929a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: ffi: ^2.0.2 isolates: ^3.0.3+8 protobuf: ^3.1.0 - talker: ^4.7.1 + logger: ^2.4.0 plugin_platform_interface: ^2.1.3 appflowy_result: path: ../appflowy_result diff --git a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml index 5d8f0d88c2..fa2e35f329 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml @@ -1,7 +1,7 @@ name: appflowy_result description: "A new Flutter package project." version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: environment: sdk: ">=3.3.0 <4.0.0" @@ -9,3 +9,40 @@ environment: dev_dependencies: flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore deleted file mode 100644 index da0bb7ce97..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -build/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/.metadata deleted file mode 100644 index 79932b61d5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" - channel: "[user-branch]" - -project_type: package diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md b/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md deleted file mode 100644 index 41cc7d8192..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE b/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE deleted file mode 100644 index ba75c69f7f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/README.md deleted file mode 100644 index 953d3545f1..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# AppFlowy UI - -AppFlowy UI is a Flutter package that provides a collection of reusable UI components following the AppFlowy design system. These components are designed to be consistent, accessible, and easy to use. - -## Features - -- **Design System Components**: Buttons, text fields, and more UI components that follow the AppFlowy design system -- **Theming**: Consistent theming across all components with light and dark mode support - -## Installation - -Add the following to your `pubspec.yaml` file: - -```yaml -dependencies: - appflowy_ui: ^1.0.0 -``` - -## Supported components - -- [x] Button -- [x] TextField -- [ ] Avatar -- [ ] Checkbox -- [ ] Grid -- [ ] Link -- [ ] Loading & Progress Indicator -- [ ] Menu -- [ ] Message Box -- [ ] Navigation Bar -- [ ] Popover -- [ ] Scroll Bar -- [ ] Tab Bar -- [ ] Toggle -- [ ] Tooltip - -## Reference - -Figma: https://www.figma.com/design/aphWa2OgkqyIragpatdk7a/Design-System diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml deleted file mode 100644 index abba19b4fe..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -linter: - rules: - - require_trailing_commas - - - prefer_collection_literals - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - - sized_box_for_whitespace - - use_decorated_box - - - unnecessary_parenthesis - - unnecessary_await_in_return - - unnecessary_raw_strings - - - avoid_unnecessary_containers - - avoid_redundant_argument_values - - avoid_unused_constructor_parameters - - - always_declare_return_types - - - sort_constructors_first - - unawaited_futures - -errors: - invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore deleted file mode 100644 index 79c113f9b5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata deleted file mode 100644 index 777c932a64..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" - channel: "[user-branch]" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: macos - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md deleted file mode 100644 index 2ccc9e658d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# AppFlowy UI Example - -This example demonstrates how to use the `appflowy_ui` package in a Flutter application. - -## Getting Started - -To run this example: - -1. Ensure you have Flutter installed and set up on your machine -2. Clone this repository -3. Navigate to the example directory: - ```bash - cd example - ``` -4. Get the dependencies: - ```bash - flutter pub get - ``` -5. Run the example: - ```bash - flutter run - ``` - -## Features Demonstrated - -- Basic app structure using AppFlowy UI components -- Material 3 design integration -- Responsive layout - -## Project Structure - -- `lib/main.dart`: The main application file -- `pubspec.yaml`: Project dependencies and configuration - -## Additional Resources - -For more information about the AppFlowy UI package, please refer to: - -- The main package documentation -- [AppFlowy Website](https://appflowy.io) -- [AppFlowy GitHub Repository](https://github.com/AppFlowy-IO/AppFlowy) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml deleted file mode 100644 index 0d2902135c..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart deleted file mode 100644 index 0d23746ebd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import 'src/buttons/buttons_page.dart'; -import 'src/modal/modal_page.dart'; -import 'src/textfield/textfield_page.dart'; - -enum ThemeMode { - light, - dark, -} - -final themeMode = ValueNotifier(ThemeMode.light); - -void main() { - runApp( - const MyApp(), - ); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: themeMode, - builder: (context, themeMode, child) { - final themeBuilder = AppFlowyDefaultTheme(); - final themeData = - themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); - - return AnimatedAppFlowyTheme( - data: themeMode == ThemeMode.light - ? themeBuilder.light() - : themeBuilder.dark(), - child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'AppFlowy UI Example', - theme: themeData.copyWith( - visualDensity: VisualDensity.standard, - ), - home: const MyHomePage( - title: 'AppFlowy UI', - ), - ), - ); - }, - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({ - super.key, - required this.title, - }); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final tabs = [ - Tab(text: 'Button'), - Tab(text: 'TextField'), - Tab(text: 'Modal'), - ]; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return DefaultTabController( - length: tabs.length, - child: Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text( - widget.title, - style: theme.textStyle.title.enhanced( - color: theme.textColorScheme.primary, - ), - ), - actions: [ - IconButton( - icon: Icon( - Theme.of(context).brightness == Brightness.light - ? Icons.dark_mode - : Icons.light_mode, - ), - onPressed: _toggleTheme, - tooltip: 'Toggle theme', - ), - ], - ), - body: TabBarView( - children: [ - ButtonsPage(), - TextFieldPage(), - ModalPage(), - ], - ), - bottomNavigationBar: TabBar( - tabs: tabs, - ), - floatingActionButton: null, - ), - ); - } - - void _toggleTheme() { - themeMode.value = - themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart deleted file mode 100644 index 0d0c018222..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ButtonsPage extends StatelessWidget { - const ButtonsPage({super.key}); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - 'Filled Text Buttons', - [ - AFFilledTextButton.primary( - text: 'Primary Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.destructive( - text: 'Destructive Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Filled Icon Text Buttons', - [ - AFFilledButton.primary( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.add, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - const SizedBox(width: 8), - Text( - 'Primary Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFFilledButton.destructive( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - const SizedBox(width: 8), - Text( - 'Destructive Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFFilledButton.disabled( - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.block, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - 'Disabled Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Outlined Text Buttons', - [ - AFOutlinedTextButton.normal( - text: 'Normal Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFOutlinedTextButton.destructive( - text: 'Destructive Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFOutlinedTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Outlined Icon Text Buttons', - [ - AFOutlinedButton.normal( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.add, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.primary, - ), - const SizedBox(width: 8), - Text( - 'Normal Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.primary, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFOutlinedButton.destructive( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.error, - ), - const SizedBox(width: 8), - Text( - 'Destructive Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.error, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFOutlinedButton.disabled( - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.block, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - 'Disabled Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Ghost Buttons', - [ - AFGhostTextButton.primary( - text: 'Primary Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFGhostTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Button with alignment', - [ - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Left Button', - onTap: () {}, - alignment: Alignment.centerLeft, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Center Button', - onTap: () {}, - alignment: Alignment.center, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Right Button', - onTap: () {}, - alignment: Alignment.centerRight, - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Button Sizes', - [ - AFFilledTextButton.primary( - text: 'Small Button', - onTap: () {}, - size: AFButtonSize.s, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Medium Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Large Button', - onTap: () {}, - size: AFButtonSize.l, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Extra Large Button', - onTap: () {}, - size: AFButtonSize.xl, - ), - ], - ), - ], - ), - ); - } - - Widget _buildSection(String title, List children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: children, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart deleted file mode 100644 index 4a9480d1b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ModalPage extends StatefulWidget { - const ModalPage({super.key}); - - @override - State createState() => _ModalPageState(); -} - -class _ModalPageState extends State { - double width = AFModalDimension.M; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: Container( - constraints: BoxConstraints(maxWidth: 600), - padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl), - child: Column( - spacing: theme.spacing.l, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - spacing: theme.spacing.m, - mainAxisSize: MainAxisSize.min, - children: [ - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.S), - builder: (context, isHovering, disabled) { - return Text( - 'S', - style: TextStyle( - color: width == AFModalDimension.S - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.M), - builder: (context, isHovering, disabled) { - return Text( - 'M', - style: TextStyle( - color: width == AFModalDimension.M - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.L), - builder: (context, isHovering, disabled) { - return Text( - 'L', - style: TextStyle( - color: width == AFModalDimension.L - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - ], - ), - AFFilledButton.primary( - builder: (context, isHovering, disabled) { - return Text( - 'Show Modal', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ); - }, - onTap: () { - showDialog( - context: context, - barrierColor: theme.surfaceColorScheme.overlay, - builder: (context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: AFModal( - constraints: BoxConstraints( - maxWidth: width, - maxHeight: AFModalDimension.dialogHeight, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AFModalHeader( - leading: Text( - 'Header', - style: theme.textStyle.heading4.standard( - color: theme.textColorScheme.primary, - ), - ), - trailing: [ - AFGhostButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return const Icon(Icons.close); - }, - ) - ], - ), - Expanded( - child: AFModalBody( - child: Text( - 'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'), - ), - ), - AFModalFooter( - trailing: [ - AFOutlinedButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return const Text('Cancel'); - }, - ), - AFFilledButton.primary( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return Text( - 'Apply', - style: TextStyle( - color: AppFlowyTheme.of(context) - .textColorScheme - .onFill, - ), - ); - }, - ), - ], - ) - ], - )), - ); - }, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart deleted file mode 100644 index 9e3436ecd4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class TextFieldPage extends StatelessWidget { - const TextFieldPage({super.key}); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - 'TextField Sizes', - [ - AFTextField( - hintText: 'Please enter your name', - size: AFTextFieldSize.m, - ), - AFTextField( - hintText: 'Please enter your name', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with hint text', - [ - AFTextField( - hintText: 'Please enter your name', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with initial text', - [ - AFTextField( - initialText: 'https://appflowy.com', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with validator ', - [ - AFTextField( - validator: (controller) { - if (controller.text.isEmpty) { - return (true, 'This field is required'); - } - - final emailRegex = - RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegex.hasMatch(controller.text)) { - return (true, 'Please enter a valid email address'); - } - - return (false, ''); - }, - ), - ], - ), - ], - ), - ); - } - - Widget _buildSection(String title, List children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: children, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore deleted file mode 100644 index 746adbb6b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b608..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b608..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 345181d730..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "appflowy_ui_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 04d5b736e6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761412..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a3..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40f..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226d5..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e09..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72c9..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfdd..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4e0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 47821fa6d8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = appflowy_ui_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb234..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a472..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1fc5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml deleted file mode 100644 index af361ecfab..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: appflowy_ui_example -description: "Example app showcasing AppFlowy UI components and widgets" -publish_to: "none" - -version: 1.0.0+1 - -environment: - flutter: ">=3.27.4" - sdk: ">=3.3.0 <4.0.0" - -dependencies: - flutter: - sdk: flutter - appflowy_ui: - path: ../ - cupertino_icons: ^1.0.6 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - -flutter: - uses-material-design: true diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart deleted file mode 100644 index 423052a342..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:appflowy_ui_example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart deleted file mode 100644 index 974907f940..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'src/component/component.dart'; -export 'src/theme/theme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart deleted file mode 100644 index 39d5175af1..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/widgets.dart'; - -enum AFButtonSize { - s, - m, - l, - xl; - - TextStyle buildTextStyle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => theme.textStyle.body.enhanced(), - AFButtonSize.m => theme.textStyle.body.enhanced(), - AFButtonSize.l => theme.textStyle.body.enhanced(), - AFButtonSize.xl => theme.textStyle.title.enhanced(), - }; - } - - EdgeInsetsGeometry buildPadding(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => EdgeInsets.symmetric( - horizontal: theme.spacing.l, - vertical: theme.spacing.xs, - ), - AFButtonSize.m => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: theme.spacing.s, - ), - AFButtonSize.l => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: 10, // why? - ), - AFButtonSize.xl => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: 14, // why? - ), - }; - } - - double buildBorderRadius(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => theme.borderRadius.m, - AFButtonSize.m => theme.borderRadius.m, - AFButtonSize.l => 10, // why? - AFButtonSize.xl => theme.borderRadius.xl, - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart deleted file mode 100644 index 9bb36507e8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFBaseButtonColorBuilder = Color Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -typedef AFBaseButtonBorderColorBuilder = Color Function( - BuildContext context, - bool isHovering, - bool disabled, - bool isFocused, -); - -class AFBaseButton extends StatefulWidget { - const AFBaseButton({ - super.key, - required this.onTap, - required this.builder, - required this.padding, - required this.borderRadius, - this.borderColor, - this.backgroundColor, - this.ringColor, - this.disabled = false, - }); - - final VoidCallback? onTap; - - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonBorderColorBuilder? ringColor; - final AFBaseButtonColorBuilder? backgroundColor; - - final EdgeInsetsGeometry padding; - final double borderRadius; - final bool disabled; - - final Widget Function( - BuildContext context, - bool isHovering, - bool disabled, - ) builder; - - @override - State createState() => _AFBaseButtonState(); -} - -class _AFBaseButtonState extends State { - final FocusNode focusNode = FocusNode(); - - bool isHovering = false; - bool isFocused = false; - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Color borderColor = _buildBorderColor(context); - final Color backgroundColor = _buildBackgroundColor(context); - final Color ringColor = _buildRingColor(context); - - return Actions( - actions: { - ActivateIntent: CallbackAction( - onInvoke: (_) { - if (!widget.disabled) { - widget.onTap?.call(); - } - return; - }, - ), - }, - child: Focus( - focusNode: focusNode, - onFocusChange: (isFocused) { - setState(() => this.isFocused = isFocused); - }, - child: MouseRegion( - cursor: widget.disabled - ? SystemMouseCursors.basic - : SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - onTap: widget.disabled ? null : widget.onTap, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(widget.borderRadius), - border: isFocused - ? Border.all( - color: ringColor, - width: 2, - strokeAlign: BorderSide.strokeAlignOutside, - ) - : null, - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Padding( - padding: widget.padding, - child: widget.builder( - context, - isHovering, - widget.disabled, - ), - ), - ), - ), - ), - ), - ), - ); - } - - Color _buildBorderColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return widget.borderColor - ?.call(context, isHovering, widget.disabled, isFocused) ?? - theme.borderColorScheme.greyTertiary; - } - - Color _buildBackgroundColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? - theme.fillColorScheme.transparent; - } - - Color _buildRingColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - if (widget.ringColor != null) { - return widget.ringColor! - .call(context, isHovering, widget.disabled, isFocused); - } - - if (isFocused) { - return theme.borderColorScheme.themeThick.withAlpha(128); - } - - return theme.borderColorScheme.transparent; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart deleted file mode 100644 index 035307d10b..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class AFBaseTextButton extends StatelessWidget { - const AFBaseTextButton({ - super.key, - required this.text, - required this.onTap, - this.disabled = false, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.textColor, - this.backgroundColor, - this.alignment, - this.textStyle, - }); - - /// The text of the button. - final String text; - - /// Whether the button is disabled. - final bool disabled; - - /// The callback when the button is tapped. - final VoidCallback onTap; - - /// The size of the button. - final AFButtonSize size; - - /// The padding of the button. - final EdgeInsetsGeometry? padding; - - /// The border radius of the button. - final double? borderRadius; - - /// The text color of the button. - final AFBaseButtonColorBuilder? textColor; - - /// The background color of the button. - final AFBaseButtonColorBuilder? backgroundColor; - - /// The alignment of the button. - /// - /// If it's null, the button size will be the size of the text with padding. - final Alignment? alignment; - - /// The text style of the button. - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - throw UnimplementedError(); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart deleted file mode 100644 index 31a3a20b5f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Base button -export 'base_button/base.dart'; -export 'base_button/base_button.dart'; -export 'base_button/base_text_button.dart'; -// Filled buttons -export 'filled_button/filled_button.dart'; -export 'filled_button/filled_icon_text_button.dart'; -export 'filled_button/filled_text_button.dart'; -// Ghost buttons -export 'ghost_button/ghost_button.dart'; -export 'ghost_button/ghost_icon_text_button.dart'; -export 'ghost_button/ghost_text_button.dart'; -// Outlined buttons -export 'outlined_button/outlined_button.dart'; -export 'outlined_button/outlined_icon_text_button.dart'; -export 'outlined_button/outlined_text_button.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart deleted file mode 100644 index e871626b59..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFFilledButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFFilledButton extends StatelessWidget { - const AFFilledButton._({ - super.key, - required this.builder, - required this.onTap, - required this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Primary text button. - factory AFFilledButton.primary({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.themeThick; - }, - ); - } - - /// Destructive text button. - factory AFFilledButton.destructive({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.errorThick; - }, - ); - } - - /// Disabled text button. - factory AFFilledButton.disabled({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: () {}, - size: size, - disabled: true, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonColorBuilder? backgroundColor; - final AFFilledButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart deleted file mode 100644 index 04c49d0b01..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFFilledIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFFilledIconTextButton extends StatelessWidget { - const AFFilledIconTextButton._({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - }); - - /// Primary filled text button. - factory AFFilledIconTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.tertiary; - } - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.fillColorScheme.themeThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Destructive filled text button. - factory AFFilledIconTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.tertiary; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Disabled filled text button. - factory AFFilledIconTextButton.disabled({ - Key? key, - required String text, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.fillColorScheme.tertiary; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Ghost filled text button with transparent background that shows color on hover. - factory AFFilledIconTextButton.ghost({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return Colors.transparent; - } - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - return theme.textColorScheme.primary; - }, - ); - } - - final String text; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFFilledIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - backgroundColor: backgroundColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.onFill; - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconBuilder(context, isHovering, disabled), - SizedBox(width: theme.spacing.s), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart deleted file mode 100644 index d1b1d868d0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFFilledTextButton extends AFBaseTextButton { - const AFFilledTextButton({ - super.key, - required super.text, - required super.onTap, - required super.backgroundColor, - required super.textColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - super.textStyle, - }); - - /// Primary text button. - factory AFFilledTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.onFill, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.themeThick; - }, - ); - } - - /// Destructive text button. - factory AFFilledTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.onFill, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.errorThick; - }, - ); - } - - /// Disabled text button. - factory AFFilledTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.tertiary, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, - ); - } - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - AppFlowyTheme.of(context).textColorScheme.onFill; - Widget child = Text( - text, - style: textStyle ?? - size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart deleted file mode 100644 index 6300c6f5a8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFGhostButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFGhostButton extends StatelessWidget { - const AFGhostButton._({ - super.key, - required this.onTap, - required this.backgroundColor, - required this.builder, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Normal ghost button. - factory AFGhostButton.normal({ - Key? key, - required VoidCallback onTap, - required AFGhostButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFGhostButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - /// Disabled ghost button. - factory AFGhostButton.disabled({ - Key? key, - required AFGhostButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFGhostButton._( - key: key, - builder: builder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.transparent, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonColorBuilder? backgroundColor; - final AFGhostButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart deleted file mode 100644 index af65599ea3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFGhostIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFGhostIconTextButton extends StatelessWidget { - const AFGhostIconTextButton({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Primary ghost text button. - factory AFGhostIconTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - required AFGhostIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFGhostIconTextButton( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return Colors.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Disabled ghost text button. - factory AFGhostIconTextButton.disabled({ - Key? key, - required String text, - required AFGhostIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFGhostIconTextButton( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - backgroundColor: (context, isHovering, disabled) { - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.tertiary; - }, - ); - } - - final String text; - final bool disabled; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFGhostIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (context, isHovering, disabled, isFocused) { - return Colors.transparent; - }, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconBuilder( - context, - isHovering, - disabled, - ), - SizedBox(width: theme.spacing.m), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart deleted file mode 100644 index d154d67dbd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFGhostTextButton extends AFBaseTextButton { - const AFGhostTextButton({ - super.key, - required super.text, - required super.onTap, - super.textColor, - super.backgroundColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - }); - - /// Normal ghost text button. - factory AFGhostTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - }) { - return AFGhostTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Disabled ghost text button. - factory AFGhostTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - }) { - return AFGhostTextButton( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.tertiary, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.transparent, - ); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - - Widget child = Text( - text, - style: size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart deleted file mode 100644 index 205d9931d6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFOutlinedButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFOutlinedButton extends StatelessWidget { - const AFOutlinedButton._({ - super.key, - required this.onTap, - required this.builder, - this.borderColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Normal outlined button. - factory AFOutlinedButton.normal({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFOutlinedButton._( - key: key, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - /// Destructive outlined button. - factory AFOutlinedButton.destructive({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFOutlinedButton._( - key: key, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorSelect; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedButton.disabled({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFOutlinedButton._( - key: key, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonColorBuilder? backgroundColor; - - final AFOutlinedButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart deleted file mode 100644 index 350594cd46..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFOutlinedIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFOutlinedIconTextButton extends StatelessWidget { - const AFOutlinedIconTextButton._({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.borderColor, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - this.alignment = MainAxisAlignment.center, - }); - - /// Normal outlined text button. - factory AFOutlinedIconTextButton.normal({ - Key? key, - required String text, - required VoidCallback onTap, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Destructive outlined text button. - factory AFOutlinedIconTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.error - : theme.textColorScheme.error; - }, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedIconTextButton.disabled({ - Key? key, - required String text, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.tertiary - : theme.textColorScheme.primary; - }, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - final String text; - final bool disabled; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - final MainAxisAlignment alignment; - - final AFOutlinedIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - disabled: disabled, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - return Row( - mainAxisAlignment: alignment, - children: [ - iconBuilder(context, isHovering, disabled), - SizedBox(width: theme.spacing.s), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart deleted file mode 100644 index d809d981b0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFOutlinedTextButton extends AFBaseTextButton { - const AFOutlinedTextButton._({ - super.key, - required super.text, - required super.onTap, - this.borderColor, - super.textStyle, - super.textColor, - super.backgroundColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - }); - - /// Normal outlined text button. - factory AFOutlinedTextButton.normal({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Destructive outlined text button. - factory AFOutlinedTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorSelect; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.error - : theme.textColorScheme.error; - }, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.tertiary - : theme.textColorScheme.primary; - }, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - final AFBaseButtonBorderColorBuilder? borderColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - - Widget child = Text( - text, - style: textStyle ?? - size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart deleted file mode 100644 index 99fec83e57..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'button/button.dart'; -export 'separator/divider.dart'; -export 'modal/modal.dart'; -export 'textfield/textfield.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart deleted file mode 100644 index 72a7dbb5cf..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart +++ /dev/null @@ -1,9 +0,0 @@ -class AFModalDimension { - const AFModalDimension._(); - - static const double S = 400.0; - static const double M = 560.0; - static const double L = 720.0; - - static const double dialogHeight = 200.0; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart deleted file mode 100644 index 4b40aebcbd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -export 'dimension.dart'; - -class AFModal extends StatelessWidget { - const AFModal({ - super.key, - this.constraints = const BoxConstraints(), - required this.child, - }); - - final BoxConstraints constraints; - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: Padding( - padding: EdgeInsets.all(theme.spacing.xl), - child: ConstrainedBox( - constraints: constraints, - child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: theme.shadow.medium, - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - color: theme.surfaceColorScheme.primary, - ), - child: Material( - color: Colors.transparent, - child: child, - ), - ), - ), - ), - ); - } -} - -class AFModalHeader extends StatelessWidget { - const AFModalHeader({ - super.key, - required this.leading, - this.trailing = const [], - }); - - final Widget leading; - final List trailing; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.only( - top: theme.spacing.xl, - left: theme.spacing.xxl, - right: theme.spacing.xxl, - ), - child: Row( - spacing: theme.spacing.s, - children: [ - Expanded(child: leading), - ...trailing, - ], - ), - ); - } -} - -class AFModalFooter extends StatelessWidget { - const AFModalFooter({ - super.key, - this.leading = const [], - this.trailing = const [], - }); - - final List leading; - final List trailing; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.only( - bottom: theme.spacing.xl, - left: theme.spacing.xxl, - right: theme.spacing.xxl, - ), - child: Row( - spacing: theme.spacing.l, - children: [ - ...leading, - Spacer(), - ...trailing, - ], - ), - ); - } -} - -class AFModalBody extends StatelessWidget { - const AFModalBody({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.symmetric( - vertical: theme.spacing.l, - horizontal: theme.spacing.xxl, - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart deleted file mode 100644 index fa5dcd093d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/widgets.dart'; - -class AFDivider extends StatelessWidget { - const AFDivider({ - super.key, - this.axis = Axis.horizontal, - this.color, - this.thickness = 1.0, - this.spacing = 0.0, - this.startIndent = 0.0, - this.endIndent = 0.0, - }) : assert(thickness > 0.0), - assert(spacing >= 0.0), - assert(startIndent >= 0.0), - assert(endIndent >= 0.0); - - final Axis axis; - final double thickness; - final double spacing; - final double startIndent; - final double endIndent; - final Color? color; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final color = this.color ?? theme.borderColorScheme.greyTertiary; - - return switch (axis) { - Axis.horizontal => Container( - height: thickness, - color: color, - margin: EdgeInsetsDirectional.only( - start: startIndent, - end: endIndent, - top: spacing, - bottom: spacing, - ), - ), - Axis.vertical => Container( - width: thickness, - color: color, - margin: EdgeInsets.only( - left: spacing, - right: spacing, - top: startIndent, - bottom: endIndent, - ), - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart deleted file mode 100644 index 3f5ad4cfed..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:appflowy_ui/src/theme/theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFTextFieldValidator = (bool result, String errorText) Function( - TextEditingController controller, -); - -abstract class AFTextFieldState extends State { - // Error handler - void syncError({required String errorText}) {} - void clearError() {} - - /// Obscure the text. - void syncObscured(bool isObscured) {} -} - -class AFTextField extends StatefulWidget { - const AFTextField({ - super.key, - this.hintText, - this.initialText, - this.keyboardType, - this.size = AFTextFieldSize.l, - this.validator, - this.controller, - this.onChanged, - this.onSubmitted, - this.autoFocus, - this.obscureText = false, - this.suffixIconBuilder, - this.suffixIconConstraints, - }); - - /// The hint text to display when the text field is empty. - final String? hintText; - - /// The initial text to display in the text field. - final String? initialText; - - /// The type of keyboard to display. - final TextInputType? keyboardType; - - /// The size variant of the text field. - final AFTextFieldSize size; - - /// The validator to use for the text field. - final AFTextFieldValidator? validator; - - /// The controller to use for the text field. - /// - /// If it's not provided, the text field will use a new controller. - final TextEditingController? controller; - - /// The callback to call when the text field changes. - final void Function(String)? onChanged; - - /// The callback to call when the text field is submitted. - final void Function(String)? onSubmitted; - - /// Enable auto focus. - final bool? autoFocus; - - /// Obscure the text. - final bool obscureText; - - /// The trailing widget to display. - final Widget Function(BuildContext context, bool isObscured)? - suffixIconBuilder; - - /// The size of the suffix icon. - final BoxConstraints? suffixIconConstraints; - - @override - State createState() => _AFTextFieldState(); -} - -class _AFTextFieldState extends AFTextFieldState { - late final TextEditingController effectiveController; - - bool hasError = false; - String errorText = ''; - - bool isObscured = false; - - @override - void initState() { - super.initState(); - - effectiveController = widget.controller ?? TextEditingController(); - - final initialText = widget.initialText; - if (initialText != null) { - effectiveController.text = initialText; - } - - effectiveController.addListener(_validate); - - isObscured = widget.obscureText; - } - - @override - void dispose() { - effectiveController.removeListener(_validate); - if (widget.controller == null) { - effectiveController.dispose(); - } - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final borderRadius = widget.size.borderRadius(theme); - final contentPadding = widget.size.contentPadding(theme); - - final errorBorderColor = theme.borderColorScheme.errorThick; - final defaultBorderColor = theme.borderColorScheme.greyTertiary; - - Widget child = TextField( - controller: effectiveController, - keyboardType: widget.keyboardType, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - obscureText: isObscured, - onChanged: widget.onChanged, - onSubmitted: widget.onSubmitted, - autofocus: widget.autoFocus ?? false, - decoration: InputDecoration( - hintText: widget.hintText, - hintStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.tertiary, - ), - isDense: true, - constraints: BoxConstraints(), - contentPadding: contentPadding, - border: OutlineInputBorder( - borderSide: BorderSide( - color: hasError ? errorBorderColor : defaultBorderColor, - ), - borderRadius: borderRadius, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: hasError ? errorBorderColor : defaultBorderColor, - ), - borderRadius: borderRadius, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: hasError - ? errorBorderColor - : theme.borderColorScheme.themeThick, - ), - borderRadius: borderRadius, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorBorderColor, - ), - borderRadius: borderRadius, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorBorderColor, - ), - borderRadius: borderRadius, - ), - hoverColor: theme.borderColorScheme.greyTertiaryHover, - suffixIcon: widget.suffixIconBuilder?.call(context, isObscured), - suffixIconConstraints: widget.suffixIconConstraints, - ), - ); - - if (hasError && errorText.isNotEmpty) { - child = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - SizedBox(height: theme.spacing.xs), - Text( - errorText, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.error, - ), - ), - ], - ); - } - - return child; - } - - void _validate() { - final validator = widget.validator; - if (validator != null) { - final result = validator(effectiveController); - setState(() { - hasError = result.$1; - errorText = result.$2; - }); - } - } - - @override - void syncError({ - required String errorText, - }) { - setState(() { - hasError = true; - this.errorText = errorText; - }); - } - - @override - void clearError() { - setState(() { - hasError = false; - errorText = ''; - }); - } - - @override - void syncObscured(bool isObscured) { - setState(() { - this.isObscured = isObscured; - }); - } -} - -enum AFTextFieldSize { - m, - l; - - EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { - return EdgeInsets.symmetric( - vertical: switch (this) { - AFTextFieldSize.m => theme.spacing.s, - AFTextFieldSize.l => 10.0, - }, - horizontal: theme.spacing.m, - ); - } - - BorderRadius borderRadius(AppFlowyThemeData theme) { - return BorderRadius.circular( - switch (this) { - AFTextFieldSize.m => theme.borderRadius.m, - AFTextFieldSize.l => 10.0, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart deleted file mode 100644 index b8dc5a1149..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:appflowy_ui/src/theme/theme.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class AppFlowyTheme extends StatelessWidget { - const AppFlowyTheme({ - super.key, - required this.data, - required this.child, - }); - - final AppFlowyThemeData data; - final Widget child; - - static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { - final provider = maybeOf(context, listen: listen); - if (provider == null) { - throw FlutterError( - ''' - AppFlowyTheme.of() called with a context that does not contain a AppFlowyTheme.\n - No AppFlowyTheme ancestor could be found starting from the context that was passed to AppFlowyTheme.of(). - This can happen because you do not have a AppFlowyTheme widget (which introduces a AppFlowyTheme), - or it can happen if the context you use comes from a widget above this widget.\n - The context used was: $context''', - ); - } - return provider; - } - - static AppFlowyThemeData? maybeOf( - BuildContext context, { - bool listen = true, - }) { - if (listen) { - return context - .dependOnInheritedWidgetOfExactType() - ?.themeData; - } - final provider = context - .getElementForInheritedWidgetOfExactType() - ?.widget; - - return (provider as AppFlowyInheritedTheme?)?.themeData; - } - - @override - Widget build(BuildContext context) { - return AppFlowyInheritedTheme( - themeData: data, - child: child, - ); - } -} - -class AppFlowyInheritedTheme extends InheritedTheme { - const AppFlowyInheritedTheme({ - super.key, - required this.themeData, - required super.child, - }); - - final AppFlowyThemeData themeData; - - @override - Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: themeData, child: child); - } - - @override - bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - themeData != oldWidget.themeData; -} - -/// An interpolation between two [AppFlowyThemeData]s. -/// -/// This class specializes the interpolation of [Tween] to -/// call the [AppFlowyThemeData.lerp] method. -/// -/// See [Tween] for a discussion on how to use interpolation objects. -class AppFlowyThemeDataTween extends Tween { - /// Creates a [AppFlowyThemeData] tween. - /// - /// The [begin] and [end] properties must be non-null before the tween is - /// first used, but the arguments can be null if the values are going to be - /// filled in later. - AppFlowyThemeDataTween({super.begin, super.end}); - - @override - AppFlowyThemeData lerp(double t) => AppFlowyThemeData.lerp(begin!, end!, t); -} - -class AnimatedAppFlowyTheme extends ImplicitlyAnimatedWidget { - /// Creates an animated theme. - /// - /// By default, the theme transition uses a linear curve. - const AnimatedAppFlowyTheme({ - super.key, - required this.data, - super.curve, - super.duration = kThemeAnimationDuration, - super.onEnd, - required this.child, - }); - - /// Specifies the color and typography values for descendant widgets. - final AppFlowyThemeData data; - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - @override - AnimatedWidgetBaseState createState() => - _AnimatedThemeState(); -} - -class _AnimatedThemeState - extends AnimatedWidgetBaseState { - AppFlowyThemeDataTween? data; - - @override - void forEachTween(TweenVisitor visitor) { - data = visitor( - data, - widget.data, - (dynamic value) => - AppFlowyThemeDataTween(begin: value as AppFlowyThemeData), - )! as AppFlowyThemeDataTween; - } - - @override - Widget build(BuildContext context) { - return AppFlowyTheme( - data: widget.data, - child: widget.child, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder description) { - super.debugFillProperties(description); - description.add( - DiagnosticsProperty( - 'data', - data, - showName: false, - defaultValue: null, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart deleted file mode 100644 index 2bd6d619d8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ /dev/null @@ -1,658 +0,0 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-19T13:45:56.076897 -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:flutter/material.dart'; - -class AppFlowyPrimitiveTokens { - AppFlowyPrimitiveTokens._(); - - /// #f8faff - static Color get neutral100 => Color(0xFFF8FAFF); - - /// #e4e8f5 - static Color get neutral200 => Color(0xFFE4E8F5); - - /// #ced3e6 - static Color get neutral300 => Color(0xFFCED3E6); - - /// #b5bbd3 - static Color get neutral400 => Color(0xFFB5BBD3); - - /// #989eb7 - static Color get neutral500 => Color(0xFF989EB7); - - /// #6f748c - static Color get neutral600 => Color(0xFF6F748C); - - /// #54596e - static Color get neutral700 => Color(0xFF54596E); - - /// #3c3f4e - static Color get neutral800 => Color(0xFF3C3F4E); - - /// #272930 - static Color get neutral900 => Color(0xFF272930); - - /// #21232a - static Color get neutral1000 => Color(0xFF21232A); - - /// #000000 - static Color get neutralBlack => Color(0xFF000000); - - /// #00000099 - static Color get neutralAlphaBlack60 => Color(0x99000000); - - /// #ffffff - static Color get neutralWhite => Color(0xFFFFFFFF); - - /// #ffffff00 - static Color get neutralAlphaWhite0 => Color(0x00FFFFFF); - - /// #ffffff33 - static Color get neutralAlphaWhite20 => Color(0x33FFFFFF); - - /// #ffffff4d - static Color get neutralAlphaWhite30 => Color(0x4DFFFFFF); - - /// #f9fafd0d - static Color get neutralAlphaGrey10005 => Color(0x0DF9FAFD); - - /// #f9fafd1a - static Color get neutralAlphaGrey10010 => Color(0x1AF9FAFD); - - /// #1f23290d - static Color get neutralAlphaGrey100005 => Color(0x0D1F2329); - - /// #1f23291a - static Color get neutralAlphaGrey100010 => Color(0x1A1F2329); - - /// #1f2329b2 - static Color get neutralAlphaGrey100070 => Color(0xB21F2329); - - /// #1f2329cc - static Color get neutralAlphaGrey100080 => Color(0xCC1F2329); - - /// #e3f6ff - static Color get blue100 => Color(0xFFE3F6FF); - - /// #a9e2ff - static Color get blue200 => Color(0xFFA9E2FF); - - /// #80d2ff - static Color get blue300 => Color(0xFF80D2FF); - - /// #4ec1ff - static Color get blue400 => Color(0xFF4EC1FF); - - /// #00b5ff - static Color get blue500 => Color(0xFF00B5FF); - - /// #0092d6 - static Color get blue600 => Color(0xFF0092D6); - - /// #0078c0 - static Color get blue700 => Color(0xFF0078C0); - - /// #0065a9 - static Color get blue800 => Color(0xFF0065A9); - - /// #00508f - static Color get blue900 => Color(0xFF00508F); - - /// #003c77 - static Color get blue1000 => Color(0xFF003C77); - - /// #00b5ff26 - static Color get blueAlphaBlue50015 => Color(0x2600B5FF); - - /// #ecf9f5 - static Color get green100 => Color(0xFFECF9F5); - - /// #c3e5d8 - static Color get green200 => Color(0xFFC3E5D8); - - /// #9ad1bc - static Color get green300 => Color(0xFF9AD1BC); - - /// #71bd9f - static Color get green400 => Color(0xFF71BD9F); - - /// #48a982 - static Color get green500 => Color(0xFF48A982); - - /// #248569 - static Color get green600 => Color(0xFF248569); - - /// #29725d - static Color get green700 => Color(0xFF29725D); - - /// #2e6050 - static Color get green800 => Color(0xFF2E6050); - - /// #305548 - static Color get green900 => Color(0xFF305548); - - /// #305244 - static Color get green1000 => Color(0xFF305244); - - /// #f1e0ff - static Color get purple100 => Color(0xFFF1E0FF); - - /// #e1b3ff - static Color get purple200 => Color(0xFFE1B3FF); - - /// #d185ff - static Color get purple300 => Color(0xFFD185FF); - - /// #bc58ff - static Color get purple400 => Color(0xFFBC58FF); - - /// #9327ff - static Color get purple500 => Color(0xFF9327FF); - - /// #7a1dcc - static Color get purple600 => Color(0xFF7A1DCC); - - /// #6617b3 - static Color get purple700 => Color(0xFF6617B3); - - /// #55138f - static Color get purple800 => Color(0xFF55138F); - - /// #470c72 - static Color get purple900 => Color(0xFF470C72); - - /// #380758 - static Color get purple1000 => Color(0xFF380758); - - /// #ffe5ef - static Color get magenta100 => Color(0xFFFFE5EF); - - /// #ffb8d1 - static Color get magenta200 => Color(0xFFFFB8D1); - - /// #ff8ab2 - static Color get magenta300 => Color(0xFFFF8AB2); - - /// #ff5c93 - static Color get magenta400 => Color(0xFFFF5C93); - - /// #fb006d - static Color get magenta500 => Color(0xFFFB006D); - - /// #d2005f - static Color get magenta600 => Color(0xFFD2005F); - - /// #d2005f - static Color get magenta700 => Color(0xFFD2005F); - - /// #850040 - static Color get magenta800 => Color(0xFF850040); - - /// #610031 - static Color get magenta900 => Color(0xFF610031); - - /// #400022 - static Color get magenta1000 => Color(0xFF400022); - - /// #ffd2dd - static Color get red100 => Color(0xFFFFD2DD); - - /// #ffa5b4 - static Color get red200 => Color(0xFFFFA5B4); - - /// #ff7d87 - static Color get red300 => Color(0xFFFF7D87); - - /// #ff5050 - static Color get red400 => Color(0xFFFF5050); - - /// #f33641 - static Color get red500 => Color(0xFFF33641); - - /// #e71d32 - static Color get red600 => Color(0xFFE71D32); - - /// #ad1625 - static Color get red700 => Color(0xFFAD1625); - - /// #8c101c - static Color get red800 => Color(0xFF8C101C); - - /// #6e0a1e - static Color get red900 => Color(0xFF6E0A1E); - - /// #4c0a17 - static Color get red1000 => Color(0xFF4C0A17); - - /// #f336411a - static Color get redAlphaRed50010 => Color(0x1AF33641); - - /// #fff3d5 - static Color get orange100 => Color(0xFFFFF3D5); - - /// #ffe4ab - static Color get orange200 => Color(0xFFFFE4AB); - - /// #ffd181 - static Color get orange300 => Color(0xFFFFD181); - - /// #ffbe62 - static Color get orange400 => Color(0xFFFFBE62); - - /// #ffa02e - static Color get orange500 => Color(0xFFFFA02E); - - /// #db7e21 - static Color get orange600 => Color(0xFFDB7E21); - - /// #b75f17 - static Color get orange700 => Color(0xFFB75F17); - - /// #93450e - static Color get orange800 => Color(0xFF93450E); - - /// #7a3108 - static Color get orange900 => Color(0xFF7A3108); - - /// #602706 - static Color get orange1000 => Color(0xFF602706); - - /// #fff9b2 - static Color get yellow100 => Color(0xFFFFF9B2); - - /// #ffec66 - static Color get yellow200 => Color(0xFFFFEC66); - - /// #ffdf1a - static Color get yellow300 => Color(0xFFFFDF1A); - - /// #ffcc00 - static Color get yellow400 => Color(0xFFFFCC00); - - /// #ffce00 - static Color get yellow500 => Color(0xFFFFCE00); - - /// #e6b800 - static Color get yellow600 => Color(0xFFE6B800); - - /// #cc9f00 - static Color get yellow700 => Color(0xFFCC9F00); - - /// #b38a00 - static Color get yellow800 => Color(0xFFB38A00); - - /// #9a7500 - static Color get yellow900 => Color(0xFF9A7500); - - /// #7f6200 - static Color get yellow1000 => Color(0xFF7F6200); - - /// #fcf2f2 - static Color get subtleColorRose100 => Color(0xFFFCF2F2); - - /// #fae3e3 - static Color get subtleColorRose200 => Color(0xFFFAE3E3); - - /// #fad9d9 - static Color get subtleColorRose300 => Color(0xFFFAD9D9); - - /// #edadad - static Color get subtleColorRose400 => Color(0xFFEDADAD); - - /// #cc4e4e - static Color get subtleColorRose500 => Color(0xFFCC4E4E); - - /// #702828 - static Color get subtleColorRose600 => Color(0xFF702828); - - /// #fcf4f0 - static Color get subtleColorPapaya100 => Color(0xFFFCF4F0); - - /// #fae8de - static Color get subtleColorPapaya200 => Color(0xFFFAE8DE); - - /// #fadfd2 - static Color get subtleColorPapaya300 => Color(0xFFFADFD2); - - /// #f0bda3 - static Color get subtleColorPapaya400 => Color(0xFFF0BDA3); - - /// #d67240 - static Color get subtleColorPapaya500 => Color(0xFFD67240); - - /// #6b3215 - static Color get subtleColorPapaya600 => Color(0xFF6B3215); - - /// #fff7ed - static Color get subtleColorTangerine100 => Color(0xFFFFF7ED); - - /// #fcedd9 - static Color get subtleColorTangerine200 => Color(0xFFFCEDD9); - - /// #fae5ca - static Color get subtleColorTangerine300 => Color(0xFFFAE5CA); - - /// #f2cb99 - static Color get subtleColorTangerine400 => Color(0xFFF2CB99); - - /// #db8f2c - static Color get subtleColorTangerine500 => Color(0xFFDB8F2C); - - /// #613b0a - static Color get subtleColorTangerine600 => Color(0xFF613B0A); - - /// #fff9ec - static Color get subtleColorMango100 => Color(0xFFFFF9EC); - - /// #fcf1d7 - static Color get subtleColorMango200 => Color(0xFFFCF1D7); - - /// #fae9c3 - static Color get subtleColorMango300 => Color(0xFFFAE9C3); - - /// #f5d68e - static Color get subtleColorMango400 => Color(0xFFF5D68E); - - /// #e0a416 - static Color get subtleColorMango500 => Color(0xFFE0A416); - - /// #5c4102 - static Color get subtleColorMango600 => Color(0xFF5C4102); - - /// #fffbe8 - static Color get subtleColorLemon100 => Color(0xFFFFFBE8); - - /// #fcf5cf - static Color get subtleColorLemon200 => Color(0xFFFCF5CF); - - /// #faefb9 - static Color get subtleColorLemon300 => Color(0xFFFAEFB9); - - /// #f5e282 - static Color get subtleColorLemon400 => Color(0xFFF5E282); - - /// #e0bb00 - static Color get subtleColorLemon500 => Color(0xFFE0BB00); - - /// #574800 - static Color get subtleColorLemon600 => Color(0xFF574800); - - /// #f9fae6 - static Color get subtleColorOlive100 => Color(0xFFF9FAE6); - - /// #f6f7d0 - static Color get subtleColorOlive200 => Color(0xFFF6F7D0); - - /// #f0f2b3 - static Color get subtleColorOlive300 => Color(0xFFF0F2B3); - - /// #dbde83 - static Color get subtleColorOlive400 => Color(0xFFDBDE83); - - /// #adb204 - static Color get subtleColorOlive500 => Color(0xFFADB204); - - /// #4a4c03 - static Color get subtleColorOlive600 => Color(0xFF4A4C03); - - /// #f6f9e6 - static Color get subtleColorLime100 => Color(0xFFF6F9E6); - - /// #eef5ce - static Color get subtleColorLime200 => Color(0xFFEEF5CE); - - /// #e7f0bb - static Color get subtleColorLime300 => Color(0xFFE7F0BB); - - /// #cfdb91 - static Color get subtleColorLime400 => Color(0xFFCFDB91); - - /// #92a822 - static Color get subtleColorLime500 => Color(0xFF92A822); - - /// #414d05 - static Color get subtleColorLime600 => Color(0xFF414D05); - - /// #f4faeb - static Color get subtleColorGrass100 => Color(0xFFF4FAEB); - - /// #e9f5d7 - static Color get subtleColorGrass200 => Color(0xFFE9F5D7); - - /// #def0c5 - static Color get subtleColorGrass300 => Color(0xFFDEF0C5); - - /// #bfd998 - static Color get subtleColorGrass400 => Color(0xFFBFD998); - - /// #75a828 - static Color get subtleColorGrass500 => Color(0xFF75A828); - - /// #334d0c - static Color get subtleColorGrass600 => Color(0xFF334D0C); - - /// #f1faf0 - static Color get subtleColorForest100 => Color(0xFFF1FAF0); - - /// #e2f5df - static Color get subtleColorForest200 => Color(0xFFE2F5DF); - - /// #d7f0d3 - static Color get subtleColorForest300 => Color(0xFFD7F0D3); - - /// #a8d6a1 - static Color get subtleColorForest400 => Color(0xFFA8D6A1); - - /// #49a33b - static Color get subtleColorForest500 => Color(0xFF49A33B); - - /// #1e4f16 - static Color get subtleColorForest600 => Color(0xFF1E4F16); - - /// #f0faf6 - static Color get subtleColorJade100 => Color(0xFFF0FAF6); - - /// #dff5eb - static Color get subtleColorJade200 => Color(0xFFDFF5EB); - - /// #cef0e1 - static Color get subtleColorJade300 => Color(0xFFCEF0E1); - - /// #90d1b5 - static Color get subtleColorJade400 => Color(0xFF90D1B5); - - /// #1c9963 - static Color get subtleColorJade500 => Color(0xFF1C9963); - - /// #075231 - static Color get subtleColorJade600 => Color(0xFF075231); - - /// #f0f9fa - static Color get subtleColorAqua100 => Color(0xFFF0F9FA); - - /// #dff3f5 - static Color get subtleColorAqua200 => Color(0xFFDFF3F5); - - /// #ccecf0 - static Color get subtleColorAqua300 => Color(0xFFCCECF0); - - /// #83ccd4 - static Color get subtleColorAqua400 => Color(0xFF83CCD4); - - /// #008e9e - static Color get subtleColorAqua500 => Color(0xFF008E9E); - - /// #004e57 - static Color get subtleColorAqua600 => Color(0xFF004E57); - - /// #f0f6fa - static Color get subtleColorAzure100 => Color(0xFFF0F6FA); - - /// #e1eef7 - static Color get subtleColorAzure200 => Color(0xFFE1EEF7); - - /// #d3e6f5 - static Color get subtleColorAzure300 => Color(0xFFD3E6F5); - - /// #88c0eb - static Color get subtleColorAzure400 => Color(0xFF88C0EB); - - /// #0877cc - static Color get subtleColorAzure500 => Color(0xFF0877CC); - - /// #154469 - static Color get subtleColorAzure600 => Color(0xFF154469); - - /// #f0f3fa - static Color get subtleColorDenim100 => Color(0xFFF0F3FA); - - /// #e3ebfa - static Color get subtleColorDenim200 => Color(0xFFE3EBFA); - - /// #d7e2f7 - static Color get subtleColorDenim300 => Color(0xFFD7E2F7); - - /// #9ab6ed - static Color get subtleColorDenim400 => Color(0xFF9AB6ED); - - /// #3267d1 - static Color get subtleColorDenim500 => Color(0xFF3267D1); - - /// #223c70 - static Color get subtleColorDenim600 => Color(0xFF223C70); - - /// #f2f2fc - static Color get subtleColorMauve100 => Color(0xFFF2F2FC); - - /// #e6e6fa - static Color get subtleColorMauve200 => Color(0xFFE6E6FA); - - /// #dcdcf7 - static Color get subtleColorMauve300 => Color(0xFFDCDCF7); - - /// #aeaef5 - static Color get subtleColorMauve400 => Color(0xFFAEAEF5); - - /// #5555e0 - static Color get subtleColorMauve500 => Color(0xFF5555E0); - - /// #36366b - static Color get subtleColorMauve600 => Color(0xFF36366B); - - /// #f6f3fc - static Color get subtleColorLavender100 => Color(0xFFF6F3FC); - - /// #ebe3fa - static Color get subtleColorLavender200 => Color(0xFFEBE3FA); - - /// #e4daf7 - static Color get subtleColorLavender300 => Color(0xFFE4DAF7); - - /// #c1aaf0 - static Color get subtleColorLavender400 => Color(0xFFC1AAF0); - - /// #8153db - static Color get subtleColorLavender500 => Color(0xFF8153DB); - - /// #462f75 - static Color get subtleColorLavender600 => Color(0xFF462F75); - - /// #f7f0fa - static Color get subtleColorLilac100 => Color(0xFFF7F0FA); - - /// #f0e1f7 - static Color get subtleColorLilac200 => Color(0xFFF0E1F7); - - /// #edd7f7 - static Color get subtleColorLilac300 => Color(0xFFEDD7F7); - - /// #d3a9e8 - static Color get subtleColorLilac400 => Color(0xFFD3A9E8); - - /// #9e4cc7 - static Color get subtleColorLilac500 => Color(0xFF9E4CC7); - - /// #562d6b - static Color get subtleColorLilac600 => Color(0xFF562D6B); - - /// #faf0fa - static Color get subtleColorMallow100 => Color(0xFFFAF0FA); - - /// #f5e1f4 - static Color get subtleColorMallow200 => Color(0xFFF5E1F4); - - /// #f5d7f4 - static Color get subtleColorMallow300 => Color(0xFFF5D7F4); - - /// #dea4dc - static Color get subtleColorMallow400 => Color(0xFFDEA4DC); - - /// #b240af - static Color get subtleColorMallow500 => Color(0xFFB240AF); - - /// #632861 - static Color get subtleColorMallow600 => Color(0xFF632861); - - /// #f9eff3 - static Color get subtleColorCamellia100 => Color(0xFFF9EFF3); - - /// #f7e1eb - static Color get subtleColorCamellia200 => Color(0xFFF7E1EB); - - /// #f7d7e5 - static Color get subtleColorCamellia300 => Color(0xFFF7D7E5); - - /// #e5a3c0 - static Color get subtleColorCamellia400 => Color(0xFFE5A3C0); - - /// #c24279 - static Color get subtleColorCamellia500 => Color(0xFFC24279); - - /// #6e2343 - static Color get subtleColorCamellia600 => Color(0xFF6E2343); - - /// #f5f5f5 - static Color get subtleColorSmoke100 => Color(0xFFF5F5F5); - - /// #e8e8e8 - static Color get subtleColorSmoke200 => Color(0xFFE8E8E8); - - /// #dedede - static Color get subtleColorSmoke300 => Color(0xFFDEDEDE); - - /// #b8b8b8 - static Color get subtleColorSmoke400 => Color(0xFFB8B8B8); - - /// #6e6e6e - static Color get subtleColorSmoke500 => Color(0xFF6E6E6E); - - /// #404040 - static Color get subtleColorSmoke600 => Color(0xFF404040); - - /// #f2f4f7 - static Color get subtleColorIron100 => Color(0xFFF2F4F7); - - /// #e6e9f0 - static Color get subtleColorIron200 => Color(0xFFE6E9F0); - - /// #dadee5 - static Color get subtleColorIron300 => Color(0xFFDADEE5); - - /// #b0b5bf - static Color get subtleColorIron400 => Color(0xFFB0B5BF); - - /// #666f80 - static Color get subtleColorIron500 => Color(0xFF666F80); - - /// #394152 - static Color get subtleColorIron600 => Color(0xFF394152); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart deleted file mode 100644 index 3c97c06df3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ /dev/null @@ -1,332 +0,0 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-19T13:45:56.089922 -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import '../shared.dart'; -import 'primitive.dart'; - -class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { - @override - AppFlowyThemeData light({ - String? fontFamily, - }) { - final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.light); - - final textColorScheme = AppFlowyTextColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral600, - tertiary: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral200, - inverse: AppFlowyPrimitiveTokens.neutralWhite, - onFill: AppFlowyPrimitiveTokens.neutralWhite, - theme: AppFlowyPrimitiveTokens.blue500, - themeHover: AppFlowyPrimitiveTokens.blue600, - action: AppFlowyPrimitiveTokens.blue500, - actionHover: AppFlowyPrimitiveTokens.blue600, - info: AppFlowyPrimitiveTokens.blue500, - infoHover: AppFlowyPrimitiveTokens.blue600, - success: AppFlowyPrimitiveTokens.green600, - successHover: AppFlowyPrimitiveTokens.green700, - warning: AppFlowyPrimitiveTokens.orange600, - warningHover: AppFlowyPrimitiveTokens.orange700, - error: AppFlowyPrimitiveTokens.red600, - errorHover: AppFlowyPrimitiveTokens.red700, - purple: AppFlowyPrimitiveTokens.purple500, - purpleHover: AppFlowyPrimitiveTokens.purple600, - ); - - final iconColorScheme = AppFlowyIconColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral600, - tertiary: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral200, - white: AppFlowyPrimitiveTokens.neutralWhite, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final borderColorScheme = AppFlowyBorderColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - greyPrimary: AppFlowyPrimitiveTokens.neutral1000, - greyPrimaryHover: AppFlowyPrimitiveTokens.neutral900, - greySecondary: AppFlowyPrimitiveTokens.neutral800, - greySecondaryHover: AppFlowyPrimitiveTokens.neutral700, - greyTertiary: AppFlowyPrimitiveTokens.neutral300, - greyTertiaryHover: AppFlowyPrimitiveTokens.neutral400, - greyQuaternary: AppFlowyPrimitiveTokens.neutral100, - greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral200, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red700, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final fillColorScheme = AppFlowyFillColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - primaryHover: AppFlowyPrimitiveTokens.neutral900, - secondary: AppFlowyPrimitiveTokens.neutral600, - secondaryHover: AppFlowyPrimitiveTokens.neutral500, - tertiary: AppFlowyPrimitiveTokens.neutral300, - tertiaryHover: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral100, - quaternaryHover: AppFlowyPrimitiveTokens.neutral200, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey100005, - primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100010, - primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, - primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, - white: AppFlowyPrimitiveTokens.neutralWhite, - whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, - whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, - black: AppFlowyPrimitiveTokens.neutralBlack, - themeLight: AppFlowyPrimitiveTokens.blue100, - themeLightHover: AppFlowyPrimitiveTokens.blue200, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, - infoLight: AppFlowyPrimitiveTokens.blue100, - infoLightHover: AppFlowyPrimitiveTokens.blue200, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successLight: AppFlowyPrimitiveTokens.green100, - successLightHover: AppFlowyPrimitiveTokens.green200, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningLight: AppFlowyPrimitiveTokens.orange100, - warningLightHover: AppFlowyPrimitiveTokens.orange200, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorLight: AppFlowyPrimitiveTokens.red100, - errorLightHover: AppFlowyPrimitiveTokens.red200, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red700, - errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, - purpleLight: AppFlowyPrimitiveTokens.purple100, - purpleLightHover: AppFlowyPrimitiveTokens.purple200, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - purpleThick: AppFlowyPrimitiveTokens.purple500, - ); - - final surfaceColorScheme = AppFlowySurfaceColorScheme( - primary: AppFlowyPrimitiveTokens.neutralWhite, - overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, - ); - - final backgroundColorScheme = AppFlowyBackgroundColorScheme( - primary: AppFlowyPrimitiveTokens.neutralWhite, - secondary: AppFlowyPrimitiveTokens.neutral100, - tertiary: AppFlowyPrimitiveTokens.neutral200, - quaternary: AppFlowyPrimitiveTokens.neutral300, - ); - - final brandColorScheme = AppFlowyBrandColorScheme( - skyline: Color(0xFF00B5FF), - aqua: Color(0xFF00C8FF), - violet: Color(0xFF9327FF), - amethyst: Color(0xFF8427E0), - berry: Color(0xFFE3006D), - coral: Color(0xFFFB006D), - golden: Color(0xFFF7931E), - amber: Color(0xFFFFBD00), - lemon: Color(0xFFFFCE00), - ); - - final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( - textHighlight: AppFlowyPrimitiveTokens.blue200, - ); - - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - } - - @override - AppFlowyThemeData dark({ - String? fontFamily, - }) { - final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark); - - final textColorScheme = AppFlowyTextColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - inverse: AppFlowyPrimitiveTokens.neutral1000, - onFill: AppFlowyPrimitiveTokens.neutralWhite, - theme: AppFlowyPrimitiveTokens.blue500, - themeHover: AppFlowyPrimitiveTokens.blue600, - action: AppFlowyPrimitiveTokens.blue500, - actionHover: AppFlowyPrimitiveTokens.blue600, - info: AppFlowyPrimitiveTokens.blue500, - infoHover: AppFlowyPrimitiveTokens.blue600, - success: AppFlowyPrimitiveTokens.green600, - successHover: AppFlowyPrimitiveTokens.green700, - warning: AppFlowyPrimitiveTokens.orange600, - warningHover: AppFlowyPrimitiveTokens.orange700, - error: AppFlowyPrimitiveTokens.red500, - errorHover: AppFlowyPrimitiveTokens.red400, - purple: AppFlowyPrimitiveTokens.purple500, - purpleHover: AppFlowyPrimitiveTokens.purple600, - ); - - final iconColorScheme = AppFlowyIconColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - white: AppFlowyPrimitiveTokens.neutralWhite, - purpleThick: Color(0xFFFFFFFF), - purpleThickHover: Color(0xFFFFFFFF), - ); - - final borderColorScheme = AppFlowyBorderColorScheme( - primary: AppFlowyPrimitiveTokens.neutral800, - greyPrimary: AppFlowyPrimitiveTokens.neutral100, - greyPrimaryHover: AppFlowyPrimitiveTokens.neutral200, - greySecondary: AppFlowyPrimitiveTokens.neutral300, - greySecondaryHover: AppFlowyPrimitiveTokens.neutral400, - greyTertiary: AppFlowyPrimitiveTokens.neutral800, - greyTertiaryHover: AppFlowyPrimitiveTokens.neutral700, - greyQuaternary: AppFlowyPrimitiveTokens.neutral1000, - greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral900, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorThick: AppFlowyPrimitiveTokens.red500, - errorThickHover: AppFlowyPrimitiveTokens.red400, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final fillColorScheme = AppFlowyFillColorScheme( - primary: AppFlowyPrimitiveTokens.neutral100, - primaryHover: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral300, - secondaryHover: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - tertiaryHover: AppFlowyPrimitiveTokens.neutral500, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - quaternaryHover: AppFlowyPrimitiveTokens.neutral900, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey10005, - primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey10010, - primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, - primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, - white: AppFlowyPrimitiveTokens.neutralWhite, - whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, - whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, - black: AppFlowyPrimitiveTokens.neutralBlack, - themeLight: AppFlowyPrimitiveTokens.blue100, - themeLightHover: AppFlowyPrimitiveTokens.blue200, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue400, - themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, - infoLight: AppFlowyPrimitiveTokens.blue100, - infoLightHover: AppFlowyPrimitiveTokens.blue200, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successLight: AppFlowyPrimitiveTokens.green100, - successLightHover: AppFlowyPrimitiveTokens.green200, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningLight: AppFlowyPrimitiveTokens.orange100, - warningLightHover: AppFlowyPrimitiveTokens.orange200, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorLight: AppFlowyPrimitiveTokens.red100, - errorLightHover: AppFlowyPrimitiveTokens.red200, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red500, - errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, - purpleLight: AppFlowyPrimitiveTokens.purple100, - purpleLightHover: AppFlowyPrimitiveTokens.purple200, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - purpleThick: AppFlowyPrimitiveTokens.purple500, - ); - - final surfaceColorScheme = AppFlowySurfaceColorScheme( - primary: AppFlowyPrimitiveTokens.neutral900, - overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, - ); - - final backgroundColorScheme = AppFlowyBackgroundColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral900, - tertiary: AppFlowyPrimitiveTokens.neutral800, - quaternary: AppFlowyPrimitiveTokens.neutral700, - ); - - final brandColorScheme = AppFlowyBrandColorScheme( - skyline: Color(0xFF00B5FF), - aqua: Color(0xFF00C8FF), - violet: Color(0xFF9327FF), - amethyst: Color(0xFF8427E0), - berry: Color(0xFFE3006D), - coral: Color(0xFFFB006D), - golden: Color(0xFFF7931E), - amber: Color(0xFFFFBD00), - lemon: Color(0xFFFFCE00), - ); - - final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( - textHighlight: AppFlowyPrimitiveTokens.blue200, - ); - - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart deleted file mode 100644 index 2b29371433..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart +++ /dev/null @@ -1 +0,0 @@ -export 'appflowy_default/semantic.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart deleted file mode 100644 index ca058310b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; - -class CustomTheme implements AppFlowyThemeBuilder { - const CustomTheme({ - required this.lightThemeJson, - required this.darkThemeJson, - }); - - final Map lightThemeJson; - final Map darkThemeJson; - - @override - AppFlowyThemeData light({ - String? fontFamily, - }) { - throw UnimplementedError(); - } - - @override - AppFlowyThemeData dark({ - String? fontFamily, - }) { - throw UnimplementedError(); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart deleted file mode 100644 index c9c3c3adb0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy_ui/src/theme/definition/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/definition/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/definition/spacing/spacing.dart'; -import 'package:flutter/material.dart'; - -class AppFlowySpacingConstant { - static const double spacing100 = 4; - static const double spacing200 = 6; - static const double spacing300 = 8; - static const double spacing400 = 12; - static const double spacing500 = 16; - static const double spacing600 = 20; -} - -class AppFlowyBorderRadiusConstant { - static const double radius100 = 4; - static const double radius200 = 6; - static const double radius300 = 8; - static const double radius400 = 12; - static const double radius500 = 16; - static const double radius600 = 20; -} - -class AppFlowySharedTokens { - const AppFlowySharedTokens(); - - static AppFlowyBorderRadius buildBorderRadius() { - return AppFlowyBorderRadius( - xs: AppFlowyBorderRadiusConstant.radius100, - s: AppFlowyBorderRadiusConstant.radius200, - m: AppFlowyBorderRadiusConstant.radius300, - l: AppFlowyBorderRadiusConstant.radius400, - xl: AppFlowyBorderRadiusConstant.radius500, - xxl: AppFlowyBorderRadiusConstant.radius600, - ); - } - - static AppFlowySpacing buildSpacing() { - return AppFlowySpacing( - xs: AppFlowySpacingConstant.spacing100, - s: AppFlowySpacingConstant.spacing200, - m: AppFlowySpacingConstant.spacing300, - l: AppFlowySpacingConstant.spacing400, - xl: AppFlowySpacingConstant.spacing500, - xxl: AppFlowySpacingConstant.spacing600, - ); - } - - static AppFlowyShadow buildShadow( - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyShadow( - small: [ - BoxShadow( - offset: Offset(0, 2), - blurRadius: 16, - color: Color(0x1F000000), - ), - ], - medium: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 32, - color: Color(0x1F000000), - ), - ], - ), - Brightness.dark => AppFlowyShadow( - small: [ - BoxShadow( - offset: Offset(0, 2), - blurRadius: 16, - color: Color(0x7A000000), - ), - ], - medium: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 32, - color: Color(0x7A000000), - ), - ], - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart deleted file mode 100644 index fb07a5fe64..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowyBorderRadius { - const AppFlowyBorderRadius({ - required this.xs, - required this.s, - required this.m, - required this.l, - required this.xl, - required this.xxl, - }); - - final double xs; - final double s; - final double m; - final double l; - final double xl; - final double xxl; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart deleted file mode 100644 index c7324c34fe..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBackgroundColorScheme { - const AppFlowyBackgroundColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - - AppFlowyBackgroundColorScheme lerp( - AppFlowyBackgroundColorScheme other, - double t, - ) { - return AppFlowyBackgroundColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart deleted file mode 100644 index ca65ed1fb4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBorderColorScheme { - AppFlowyBorderColorScheme({ - required this.primary, - required this.greyPrimary, - required this.greyPrimaryHover, - required this.greySecondary, - required this.greySecondaryHover, - required this.greyTertiary, - required this.greyTertiaryHover, - required this.greyQuaternary, - required this.greyQuaternaryHover, - required this.transparent, - required this.themeThick, - required this.themeThickHover, - required this.infoThick, - required this.infoThickHover, - required this.successThick, - required this.successThickHover, - required this.warningThick, - required this.warningThickHover, - required this.errorThick, - required this.errorThickHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color greyPrimary; - final Color greyPrimaryHover; - final Color greySecondary; - final Color greySecondaryHover; - final Color greyTertiary; - final Color greyTertiaryHover; - final Color greyQuaternary; - final Color greyQuaternaryHover; - final Color transparent; - final Color themeThick; - final Color themeThickHover; - final Color infoThick; - final Color infoThickHover; - final Color successThick; - final Color successThickHover; - final Color warningThick; - final Color warningThickHover; - final Color errorThick; - final Color errorThickHover; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyBorderColorScheme lerp( - AppFlowyBorderColorScheme other, - double t, - ) { - return AppFlowyBorderColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - greyPrimary: Color.lerp(greyPrimary, other.greyPrimary, t)!, - greyPrimaryHover: - Color.lerp(greyPrimaryHover, other.greyPrimaryHover, t)!, - greySecondary: Color.lerp(greySecondary, other.greySecondary, t)!, - greySecondaryHover: - Color.lerp(greySecondaryHover, other.greySecondaryHover, t)!, - greyTertiary: Color.lerp(greyTertiary, other.greyTertiary, t)!, - greyTertiaryHover: - Color.lerp(greyTertiaryHover, other.greyTertiaryHover, t)!, - greyQuaternary: Color.lerp(greyQuaternary, other.greyQuaternary, t)!, - greyQuaternaryHover: - Color.lerp(greyQuaternaryHover, other.greyQuaternaryHover, t)!, - transparent: Color.lerp(transparent, other.transparent, t)!, - themeThick: Color.lerp(themeThick, other.themeThick, t)!, - themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, - infoThick: Color.lerp(infoThick, other.infoThick, t)!, - infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, - successThick: Color.lerp(successThick, other.successThick, t)!, - successThickHover: - Color.lerp(successThickHover, other.successThickHover, t)!, - warningThick: Color.lerp(warningThick, other.warningThick, t)!, - warningThickHover: - Color.lerp(warningThickHover, other.warningThickHover, t)!, - errorThick: Color.lerp(errorThick, other.errorThick, t)!, - errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart deleted file mode 100644 index 4140f6924a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBrandColorScheme { - const AppFlowyBrandColorScheme({ - required this.skyline, - required this.aqua, - required this.violet, - required this.amethyst, - required this.berry, - required this.coral, - required this.golden, - required this.amber, - required this.lemon, - }); - - final Color skyline; - final Color aqua; - final Color violet; - final Color amethyst; - final Color berry; - final Color coral; - final Color golden; - final Color amber; - final Color lemon; - - AppFlowyBrandColorScheme lerp( - AppFlowyBrandColorScheme other, - double t, - ) { - return AppFlowyBrandColorScheme( - skyline: Color.lerp(skyline, other.skyline, t)!, - aqua: Color.lerp(aqua, other.aqua, t)!, - violet: Color.lerp(violet, other.violet, t)!, - amethyst: Color.lerp(amethyst, other.amethyst, t)!, - berry: Color.lerp(berry, other.berry, t)!, - coral: Color.lerp(coral, other.coral, t)!, - golden: Color.lerp(golden, other.golden, t)!, - amber: Color.lerp(amber, other.amber, t)!, - lemon: Color.lerp(lemon, other.lemon, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart deleted file mode 100644 index 01952e1461..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'background_color_scheme.dart'; -export 'border_color_scheme.dart'; -export 'brand_color_scheme.dart'; -export 'fill_color_scheme.dart'; -export 'icon_color_scheme.dart'; -export 'other_color_scheme.dart'; -export 'surface_color_scheme.dart'; -export 'text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart deleted file mode 100644 index 3faac64dfc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyFillColorScheme { - const AppFlowyFillColorScheme({ - required this.primary, - required this.primaryHover, - required this.secondary, - required this.secondaryHover, - required this.tertiary, - required this.tertiaryHover, - required this.quaternary, - required this.quaternaryHover, - required this.transparent, - required this.primaryAlpha5, - required this.primaryAlpha5Hover, - required this.primaryAlpha80, - required this.primaryAlpha80Hover, - required this.white, - required this.whiteAlpha, - required this.whiteAlphaHover, - required this.black, - required this.themeLight, - required this.themeLightHover, - required this.themeThick, - required this.themeThickHover, - required this.themeSelect, - required this.infoLight, - required this.infoLightHover, - required this.infoThick, - required this.infoThickHover, - required this.successLight, - required this.successLightHover, - required this.successThick, - required this.successThickHover, - required this.warningLight, - required this.warningLightHover, - required this.warningThick, - required this.warningThickHover, - required this.errorLight, - required this.errorLightHover, - required this.errorThick, - required this.errorThickHover, - required this.errorSelect, - required this.purpleLight, - required this.purpleLightHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color primaryHover; - final Color secondary; - final Color secondaryHover; - final Color tertiary; - final Color tertiaryHover; - final Color quaternary; - final Color quaternaryHover; - final Color transparent; - final Color primaryAlpha5; - final Color primaryAlpha5Hover; - final Color primaryAlpha80; - final Color primaryAlpha80Hover; - final Color white; - final Color whiteAlpha; - final Color whiteAlphaHover; - final Color black; - final Color themeLight; - final Color themeLightHover; - final Color themeThick; - final Color themeThickHover; - final Color themeSelect; - final Color infoLight; - final Color infoLightHover; - final Color infoThick; - final Color infoThickHover; - final Color successLight; - final Color successLightHover; - final Color successThick; - final Color successThickHover; - final Color warningLight; - final Color warningLightHover; - final Color warningThick; - final Color warningThickHover; - final Color errorLight; - final Color errorLightHover; - final Color errorThick; - final Color errorThickHover; - final Color errorSelect; - final Color purpleLight; - final Color purpleLightHover; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyFillColorScheme lerp( - AppFlowyFillColorScheme other, - double t, - ) { - return AppFlowyFillColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - tertiaryHover: Color.lerp(tertiaryHover, other.tertiaryHover, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - quaternaryHover: Color.lerp(quaternaryHover, other.quaternaryHover, t)!, - transparent: Color.lerp(transparent, other.transparent, t)!, - primaryAlpha5: Color.lerp(primaryAlpha5, other.primaryAlpha5, t)!, - primaryAlpha5Hover: - Color.lerp(primaryAlpha5Hover, other.primaryAlpha5Hover, t)!, - primaryAlpha80: Color.lerp(primaryAlpha80, other.primaryAlpha80, t)!, - primaryAlpha80Hover: - Color.lerp(primaryAlpha80Hover, other.primaryAlpha80Hover, t)!, - white: Color.lerp(white, other.white, t)!, - whiteAlpha: Color.lerp(whiteAlpha, other.whiteAlpha, t)!, - whiteAlphaHover: Color.lerp(whiteAlphaHover, other.whiteAlphaHover, t)!, - black: Color.lerp(black, other.black, t)!, - themeLight: Color.lerp(themeLight, other.themeLight, t)!, - themeLightHover: Color.lerp(themeLightHover, other.themeLightHover, t)!, - themeThick: Color.lerp(themeThick, other.themeThick, t)!, - themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, - themeSelect: Color.lerp(themeSelect, other.themeSelect, t)!, - infoLight: Color.lerp(infoLight, other.infoLight, t)!, - infoLightHover: Color.lerp(infoLightHover, other.infoLightHover, t)!, - infoThick: Color.lerp(infoThick, other.infoThick, t)!, - infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, - successLight: Color.lerp(successLight, other.successLight, t)!, - successLightHover: - Color.lerp(successLightHover, other.successLightHover, t)!, - successThick: Color.lerp(successThick, other.successThick, t)!, - successThickHover: - Color.lerp(successThickHover, other.successThickHover, t)!, - warningLight: Color.lerp(warningLight, other.warningLight, t)!, - warningLightHover: - Color.lerp(warningLightHover, other.warningLightHover, t)!, - warningThick: Color.lerp(warningThick, other.warningThick, t)!, - warningThickHover: - Color.lerp(warningThickHover, other.warningThickHover, t)!, - errorLight: Color.lerp(errorLight, other.errorLight, t)!, - errorLightHover: Color.lerp(errorLightHover, other.errorLightHover, t)!, - errorThick: Color.lerp(errorThick, other.errorThick, t)!, - errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, - errorSelect: Color.lerp(errorSelect, other.errorSelect, t)!, - purpleLight: Color.lerp(purpleLight, other.purpleLight, t)!, - purpleLightHover: - Color.lerp(purpleLightHover, other.purpleLightHover, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart deleted file mode 100644 index efe59b8b99..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyIconColorScheme { - const AppFlowyIconColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - required this.white, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - final Color white; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyIconColorScheme lerp( - AppFlowyIconColorScheme other, - double t, - ) { - return AppFlowyIconColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - white: Color.lerp(white, other.white, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart deleted file mode 100644 index 9bb21e54e6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:ui'; - -class AppFlowyOtherColorsColorScheme { - const AppFlowyOtherColorsColorScheme({ - required this.textHighlight, - }); - - final Color textHighlight; - - AppFlowyOtherColorsColorScheme lerp( - AppFlowyOtherColorsColorScheme other, - double t, - ) { - return AppFlowyOtherColorsColorScheme( - textHighlight: Color.lerp(textHighlight, other.textHighlight, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart deleted file mode 100644 index 67be450a04..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowySurfaceColorScheme { - const AppFlowySurfaceColorScheme({ - required this.primary, - required this.overlay, - }); - - final Color primary; - final Color overlay; - - AppFlowySurfaceColorScheme lerp( - AppFlowySurfaceColorScheme other, - double t, - ) { - return AppFlowySurfaceColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - overlay: Color.lerp(overlay, other.overlay, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart deleted file mode 100644 index 17e1f057ce..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyTextColorScheme { - const AppFlowyTextColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - required this.inverse, - required this.onFill, - required this.theme, - required this.themeHover, - required this.action, - required this.actionHover, - required this.info, - required this.infoHover, - required this.success, - required this.successHover, - required this.warning, - required this.warningHover, - required this.error, - required this.errorHover, - required this.purple, - required this.purpleHover, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - final Color inverse; - final Color onFill; - final Color theme; - final Color themeHover; - final Color action; - final Color actionHover; - final Color info; - final Color infoHover; - final Color success; - final Color successHover; - final Color warning; - final Color warningHover; - final Color error; - final Color errorHover; - final Color purple; - final Color purpleHover; - - AppFlowyTextColorScheme lerp( - AppFlowyTextColorScheme other, - double t, - ) { - return AppFlowyTextColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - inverse: Color.lerp(inverse, other.inverse, t)!, - onFill: Color.lerp(onFill, other.onFill, t)!, - theme: Color.lerp(theme, other.theme, t)!, - themeHover: Color.lerp(themeHover, other.themeHover, t)!, - action: Color.lerp(action, other.action, t)!, - actionHover: Color.lerp(actionHover, other.actionHover, t)!, - info: Color.lerp(info, other.info, t)!, - infoHover: Color.lerp(infoHover, other.infoHover, t)!, - success: Color.lerp(success, other.success, t)!, - successHover: Color.lerp(successHover, other.successHover, t)!, - warning: Color.lerp(warning, other.warning, t)!, - warningHover: Color.lerp(warningHover, other.warningHover, t)!, - error: Color.lerp(error, other.error, t)!, - errorHover: Color.lerp(errorHover, other.errorHover, t)!, - purple: Color.lerp(purple, other.purple, t)!, - purpleHover: Color.lerp(purpleHover, other.purpleHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart deleted file mode 100644 index 457b86265e..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class AppFlowyShadow { - AppFlowyShadow({ - required this.small, - required this.medium, - }); - - final List small; - final List medium; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart deleted file mode 100644 index ea90784db3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowySpacing { - const AppFlowySpacing({ - required this.xs, - required this.s, - required this.m, - required this.l, - required this.xl, - required this.xxl, - }); - - final double xs; - final double s; - final double m; - final double l; - final double xl; - final double xxl; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart deleted file mode 100644 index 006f364f96..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart +++ /dev/null @@ -1,537 +0,0 @@ -import 'package:flutter/widgets.dart'; - -abstract class TextThemeType { - const TextThemeType({ - required this.fontFamily, - }); - - final String fontFamily; - - TextStyle standard({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle enhanced({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle prominent({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle underline({ - String? family, - Color? color, - FontWeight? weight, - }); -} - -class TextThemeHeading1 extends TextThemeType { - const TextThemeHeading1({ - required super.fontFamily, - }); - - @override - TextStyle standard({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.bold, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - required double fontSize, - required double height, - TextDecoration decoration = TextDecoration.none, - Color? color, - FontWeight weight = FontWeight.bold, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading2 extends TextThemeType { - const TextThemeHeading2({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 24, - double height = 32 / 24, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading3 extends TextThemeType { - const TextThemeHeading3({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 20, - double height = 28 / 20, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading4 extends TextThemeType { - const TextThemeHeading4({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 16, - double height = 22 / 16, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeadline extends TextThemeType { - const TextThemeHeadline({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 24, - double height = 36 / 24, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.normal, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeTitle extends TextThemeType { - const TextThemeTitle({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 20, - double height = 28 / 20, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeBody extends TextThemeType { - const TextThemeBody({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 14, - double height = 20 / 14, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeCaption extends TextThemeType { - const TextThemeCaption({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 12, - double height = 16 / 12, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart deleted file mode 100644 index 89c1278d93..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; - -class AppFlowyBaseTextStyle { - factory AppFlowyBaseTextStyle.customFontFamily(String fontFamily) => - AppFlowyBaseTextStyle( - heading1: TextThemeHeading1(fontFamily: fontFamily), - heading2: TextThemeHeading2(fontFamily: fontFamily), - heading3: TextThemeHeading3(fontFamily: fontFamily), - heading4: TextThemeHeading4(fontFamily: fontFamily), - headline: TextThemeHeadline(fontFamily: fontFamily), - title: TextThemeTitle(fontFamily: fontFamily), - body: TextThemeBody(fontFamily: fontFamily), - caption: TextThemeCaption(fontFamily: fontFamily), - ); - - const AppFlowyBaseTextStyle({ - this.heading1 = const TextThemeHeading1(fontFamily: ''), - this.heading2 = const TextThemeHeading2(fontFamily: ''), - this.heading3 = const TextThemeHeading3(fontFamily: ''), - this.heading4 = const TextThemeHeading4(fontFamily: ''), - this.headline = const TextThemeHeadline(fontFamily: ''), - this.title = const TextThemeTitle(fontFamily: ''), - this.body = const TextThemeBody(fontFamily: ''), - this.caption = const TextThemeCaption(fontFamily: ''), - }); - - final TextThemeType heading1; - final TextThemeType heading2; - final TextThemeType heading3; - final TextThemeType heading4; - final TextThemeType headline; - final TextThemeType title; - final TextThemeType body; - final TextThemeType caption; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart deleted file mode 100644 index 1da45cfd2a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'border_radius/border_radius.dart'; -import 'color_scheme/color_scheme.dart'; -import 'shadow/shadow.dart'; -import 'spacing/spacing.dart'; -import 'text_style/text_style.dart'; - -/// [AppFlowyThemeData] defines the structure of the design system, and contains -/// the data that all child widgets will have access to. -class AppFlowyThemeData { - const AppFlowyThemeData({ - required this.textColorScheme, - required this.textStyle, - required this.iconColorScheme, - required this.borderColorScheme, - required this.backgroundColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.otherColorsColorScheme, - }); - - final AppFlowyTextColorScheme textColorScheme; - - final AppFlowyBaseTextStyle textStyle; - - final AppFlowyIconColorScheme iconColorScheme; - - final AppFlowyBorderColorScheme borderColorScheme; - - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - final AppFlowyFillColorScheme fillColorScheme; - - final AppFlowySurfaceColorScheme surfaceColorScheme; - - final AppFlowyBorderRadius borderRadius; - - final AppFlowySpacing spacing; - - final AppFlowyShadow shadow; - - final AppFlowyBrandColorScheme brandColorScheme; - - final AppFlowyOtherColorsColorScheme otherColorsColorScheme; - - static AppFlowyThemeData lerp( - AppFlowyThemeData begin, - AppFlowyThemeData end, - double t, - ) { - return AppFlowyThemeData( - textColorScheme: begin.textColorScheme.lerp(end.textColorScheme, t), - textStyle: end.textStyle, - iconColorScheme: begin.iconColorScheme.lerp(end.iconColorScheme, t), - borderColorScheme: begin.borderColorScheme.lerp(end.borderColorScheme, t), - backgroundColorScheme: - begin.backgroundColorScheme.lerp(end.backgroundColorScheme, t), - fillColorScheme: begin.fillColorScheme.lerp(end.fillColorScheme, t), - surfaceColorScheme: - begin.surfaceColorScheme.lerp(end.surfaceColorScheme, t), - borderRadius: end.borderRadius, - spacing: end.spacing, - shadow: end.shadow, - brandColorScheme: begin.brandColorScheme.lerp(end.brandColorScheme, t), - otherColorsColorScheme: - begin.otherColorsColorScheme.lerp(end.otherColorsColorScheme, t), - ); - } -} - -/// [AppFlowyThemeBuilder] is used to build the light and dark themes. Extend -/// this class to create a built-in theme, or use the [CustomTheme] class to -/// create a custom theme from JSON data. -/// -/// See also: -/// -/// - [AppFlowyThemeData] for the main theme data class. -abstract class AppFlowyThemeBuilder { - const AppFlowyThemeBuilder(); - - AppFlowyThemeData light({ - String? fontFamily, - }); - - AppFlowyThemeData dark({ - String? fontFamily, - }); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart deleted file mode 100644 index 000b7a0372..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'appflowy_theme.dart'; -export 'data/built_in_themes.dart'; -export 'definition/border_radius/border_radius.dart'; -export 'definition/color_scheme/color_scheme.dart'; -export 'definition/theme_data.dart'; -export 'definition/spacing/spacing.dart'; -export 'definition/shadow/shadow.dart'; -export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml deleted file mode 100644 index 2f5633bb1e..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: appflowy_ui -description: "A Flutter package for AppFlowy UI components and widgets" -version: 1.0.0 -homepage: https://github.com/appflowy-io/appflowy - -environment: - sdk: ^3.6.2 - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_lints: ^5.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json deleted file mode 100644 index c46354b599..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json +++ /dev/null @@ -1,984 +0,0 @@ -{ - "Neutral": { - "100": { - "$type": "color", - "$value": "#f8faff" - }, - "200": { - "$type": "color", - "$value": "#e4e8f5" - }, - "300": { - "$type": "color", - "$value": "#ced3e6" - }, - "400": { - "$type": "color", - "$value": "#b5bbd3" - }, - "500": { - "$type": "color", - "$value": "#989eb7" - }, - "600": { - "$type": "color", - "$value": "#6f748c" - }, - "700": { - "$type": "color", - "$value": "#54596e" - }, - "800": { - "$type": "color", - "$value": "#3c3f4e" - }, - "900": { - "$type": "color", - "$value": "#272930" - }, - "1000": { - "$type": "color", - "$value": "#21232a" - }, - "black": { - "$type": "color", - "$value": "#000000" - }, - "alpha-black-60": { - "$type": "color", - "$value": "#00000099" - }, - "white": { - "$type": "color", - "$value": "#ffffff" - }, - "alpha-white-0": { - "$type": "color", - "$value": "#ffffff00" - }, - "alpha-white-20": { - "$type": "color", - "$value": "#ffffff33" - }, - "alpha-white-30": { - "$type": "color", - "$value": "#ffffff4d" - }, - "alpha-grey-100-05": { - "$type": "color", - "$value": "#f9fafd0d" - }, - "alpha-grey-100-10": { - "$type": "color", - "$value": "#f9fafd1a" - }, - "alpha-grey-1000-05": { - "$type": "color", - "$value": "#1f23290d" - }, - "alpha-grey-1000-10": { - "$type": "color", - "$value": "#1f23291a" - }, - "alpha-grey-1000-70": { - "$type": "color", - "$value": "#1f2329b2" - }, - "alpha-grey-1000-80": { - "$type": "color", - "$value": "#1f2329cc" - } - }, - "Blue": { - "100": { - "$type": "color", - "$value": "#e3f6ff" - }, - "200": { - "$type": "color", - "$value": "#a9e2ff" - }, - "300": { - "$type": "color", - "$value": "#80d2ff" - }, - "400": { - "$type": "color", - "$value": "#4ec1ff" - }, - "500": { - "$type": "color", - "$value": "#00b5ff" - }, - "600": { - "$type": "color", - "$value": "#0092d6" - }, - "700": { - "$type": "color", - "$value": "#0078c0" - }, - "800": { - "$type": "color", - "$value": "#0065a9" - }, - "900": { - "$type": "color", - "$value": "#00508f" - }, - "1000": { - "$type": "color", - "$value": "#003c77" - }, - "alpha-blue-500-15": { - "$type": "color", - "$value": "#00b5ff26" - } - }, - "Green": { - "100": { - "$type": "color", - "$value": "#ecf9f5" - }, - "200": { - "$type": "color", - "$value": "#c3e5d8" - }, - "300": { - "$type": "color", - "$value": "#9ad1bc" - }, - "400": { - "$type": "color", - "$value": "#71bd9f" - }, - "500": { - "$type": "color", - "$value": "#48a982" - }, - "600": { - "$type": "color", - "$value": "#248569" - }, - "700": { - "$type": "color", - "$value": "#29725d" - }, - "800": { - "$type": "color", - "$value": "#2e6050" - }, - "900": { - "$type": "color", - "$value": "#305548" - }, - "1000": { - "$type": "color", - "$value": "#305244" - } - }, - "Purple": { - "100": { - "$type": "color", - "$value": "#f1e0ff" - }, - "200": { - "$type": "color", - "$value": "#e1b3ff" - }, - "300": { - "$type": "color", - "$value": "#d185ff" - }, - "400": { - "$type": "color", - "$value": "#bc58ff" - }, - "500": { - "$type": "color", - "$value": "#9327ff" - }, - "600": { - "$type": "color", - "$value": "#7a1dcc" - }, - "700": { - "$type": "color", - "$value": "#6617b3" - }, - "800": { - "$type": "color", - "$value": "#55138f" - }, - "900": { - "$type": "color", - "$value": "#470c72" - }, - "1000": { - "$type": "color", - "$value": "#380758" - } - }, - "Magenta": { - "100": { - "$type": "color", - "$value": "#ffe5ef" - }, - "200": { - "$type": "color", - "$value": "#ffb8d1" - }, - "300": { - "$type": "color", - "$value": "#ff8ab2" - }, - "400": { - "$type": "color", - "$value": "#ff5c93" - }, - "500": { - "$type": "color", - "$value": "#fb006d" - }, - "600": { - "$type": "color", - "$value": "#d2005f" - }, - "700": { - "$type": "color", - "$value": "#d2005f" - }, - "800": { - "$type": "color", - "$value": "#850040" - }, - "900": { - "$type": "color", - "$value": "#610031" - }, - "1000": { - "$type": "color", - "$value": "#400022" - } - }, - "Red": { - "100": { - "$type": "color", - "$value": "#ffd2dd" - }, - "200": { - "$type": "color", - "$value": "#ffa5b4" - }, - "300": { - "$type": "color", - "$value": "#ff7d87" - }, - "400": { - "$type": "color", - "$value": "#ff5050" - }, - "500": { - "$type": "color", - "$value": "#f33641" - }, - "600": { - "$type": "color", - "$value": "#e71d32" - }, - "700": { - "$type": "color", - "$value": "#ad1625" - }, - "800": { - "$type": "color", - "$value": "#8c101c" - }, - "900": { - "$type": "color", - "$value": "#6e0a1e" - }, - "1000": { - "$type": "color", - "$value": "#4c0a17" - }, - "alpha-red-500-10": { - "$type": "color", - "$value": "#f336411a" - } - }, - "Orange": { - "100": { - "$type": "color", - "$value": "#fff3d5" - }, - "200": { - "$type": "color", - "$value": "#ffe4ab" - }, - "300": { - "$type": "color", - "$value": "#ffd181" - }, - "400": { - "$type": "color", - "$value": "#ffbe62" - }, - "500": { - "$type": "color", - "$value": "#ffa02e" - }, - "600": { - "$type": "color", - "$value": "#db7e21" - }, - "700": { - "$type": "color", - "$value": "#b75f17" - }, - "800": { - "$type": "color", - "$value": "#93450e" - }, - "900": { - "$type": "color", - "$value": "#7a3108" - }, - "1000": { - "$type": "color", - "$value": "#602706" - } - }, - "Yellow": { - "100": { - "$type": "color", - "$value": "#fff9b2" - }, - "200": { - "$type": "color", - "$value": "#ffec66" - }, - "300": { - "$type": "color", - "$value": "#ffdf1a" - }, - "400": { - "$type": "color", - "$value": "#ffcc00" - }, - "500": { - "$type": "color", - "$value": "#ffce00" - }, - "600": { - "$type": "color", - "$value": "#e6b800" - }, - "700": { - "$type": "color", - "$value": "#cc9f00" - }, - "800": { - "$type": "color", - "$value": "#b38a00" - }, - "900": { - "$type": "color", - "$value": "#9a7500" - }, - "1000": { - "$type": "color", - "$value": "#7f6200" - } - }, - "Subtle_Color": { - "Rose": { - "100": { - "$type": "color", - "$value": "#fcf2f2" - }, - "200": { - "$type": "color", - "$value": "#fae3e3" - }, - "300": { - "$type": "color", - "$value": "#fad9d9" - }, - "400": { - "$type": "color", - "$value": "#edadad" - }, - "500": { - "$type": "color", - "$value": "#cc4e4e" - }, - "600": { - "$type": "color", - "$value": "#702828" - } - }, - "Papaya": { - "100": { - "$type": "color", - "$value": "#fcf4f0" - }, - "200": { - "$type": "color", - "$value": "#fae8de" - }, - "300": { - "$type": "color", - "$value": "#fadfd2" - }, - "400": { - "$type": "color", - "$value": "#f0bda3" - }, - "500": { - "$type": "color", - "$value": "#d67240" - }, - "600": { - "$type": "color", - "$value": "#6b3215" - } - }, - "Tangerine": { - "100": { - "$type": "color", - "$value": "#fff7ed" - }, - "200": { - "$type": "color", - "$value": "#fcedd9" - }, - "300": { - "$type": "color", - "$value": "#fae5ca" - }, - "400": { - "$type": "color", - "$value": "#f2cb99" - }, - "500": { - "$type": "color", - "$value": "#db8f2c" - }, - "600": { - "$type": "color", - "$value": "#613b0a" - } - }, - "Mango": { - "100": { - "$type": "color", - "$value": "#fff9ec" - }, - "200": { - "$type": "color", - "$value": "#fcf1d7" - }, - "300": { - "$type": "color", - "$value": "#fae9c3" - }, - "400": { - "$type": "color", - "$value": "#f5d68e" - }, - "500": { - "$type": "color", - "$value": "#e0a416" - }, - "600": { - "$type": "color", - "$value": "#5c4102" - } - }, - "Lemon": { - "100": { - "$type": "color", - "$value": "#fffbe8" - }, - "200": { - "$type": "color", - "$value": "#fcf5cf" - }, - "300": { - "$type": "color", - "$value": "#faefb9" - }, - "400": { - "$type": "color", - "$value": "#f5e282" - }, - "500": { - "$type": "color", - "$value": "#e0bb00" - }, - "600": { - "$type": "color", - "$value": "#574800" - } - }, - "Olive": { - "100": { - "$type": "color", - "$value": "#f9fae6" - }, - "200": { - "$type": "color", - "$value": "#f6f7d0" - }, - "300": { - "$type": "color", - "$value": "#f0f2b3" - }, - "400": { - "$type": "color", - "$value": "#dbde83" - }, - "500": { - "$type": "color", - "$value": "#adb204" - }, - "600": { - "$type": "color", - "$value": "#4a4c03" - } - }, - "Lime": { - "100": { - "$type": "color", - "$value": "#f6f9e6" - }, - "200": { - "$type": "color", - "$value": "#eef5ce" - }, - "300": { - "$type": "color", - "$value": "#e7f0bb" - }, - "400": { - "$type": "color", - "$value": "#cfdb91" - }, - "500": { - "$type": "color", - "$value": "#92a822" - }, - "600": { - "$type": "color", - "$value": "#414d05" - } - }, - "Grass": { - "100": { - "$type": "color", - "$value": "#f4faeb" - }, - "200": { - "$type": "color", - "$value": "#e9f5d7" - }, - "300": { - "$type": "color", - "$value": "#def0c5" - }, - "400": { - "$type": "color", - "$value": "#bfd998" - }, - "500": { - "$type": "color", - "$value": "#75a828" - }, - "600": { - "$type": "color", - "$value": "#334d0c" - } - }, - "Forest": { - "100": { - "$type": "color", - "$value": "#f1faf0" - }, - "200": { - "$type": "color", - "$value": "#e2f5df" - }, - "300": { - "$type": "color", - "$value": "#d7f0d3" - }, - "400": { - "$type": "color", - "$value": "#a8d6a1" - }, - "500": { - "$type": "color", - "$value": "#49a33b" - }, - "600": { - "$type": "color", - "$value": "#1e4f16" - } - }, - "Jade": { - "100": { - "$type": "color", - "$value": "#f0faf6" - }, - "200": { - "$type": "color", - "$value": "#dff5eb" - }, - "300": { - "$type": "color", - "$value": "#cef0e1" - }, - "400": { - "$type": "color", - "$value": "#90d1b5" - }, - "500": { - "$type": "color", - "$value": "#1c9963" - }, - "600": { - "$type": "color", - "$value": "#075231" - } - }, - "Aqua": { - "100": { - "$type": "color", - "$value": "#f0f9fa" - }, - "200": { - "$type": "color", - "$value": "#dff3f5" - }, - "300": { - "$type": "color", - "$value": "#ccecf0" - }, - "400": { - "$type": "color", - "$value": "#83ccd4" - }, - "500": { - "$type": "color", - "$value": "#008e9e" - }, - "600": { - "$type": "color", - "$value": "#004e57" - } - }, - "Azure": { - "100": { - "$type": "color", - "$value": "#f0f6fa" - }, - "200": { - "$type": "color", - "$value": "#e1eef7" - }, - "300": { - "$type": "color", - "$value": "#d3e6f5" - }, - "400": { - "$type": "color", - "$value": "#88c0eb" - }, - "500": { - "$type": "color", - "$value": "#0877cc" - }, - "600": { - "$type": "color", - "$value": "#154469" - } - }, - "Denim": { - "100": { - "$type": "color", - "$value": "#f0f3fa" - }, - "200": { - "$type": "color", - "$value": "#e3ebfa" - }, - "300": { - "$type": "color", - "$value": "#d7e2f7" - }, - "400": { - "$type": "color", - "$value": "#9ab6ed" - }, - "500": { - "$type": "color", - "$value": "#3267d1" - }, - "600": { - "$type": "color", - "$value": "#223c70" - } - }, - "Mauve": { - "100": { - "$type": "color", - "$value": "#f2f2fc" - }, - "200": { - "$type": "color", - "$value": "#e6e6fa" - }, - "300": { - "$type": "color", - "$value": "#dcdcf7" - }, - "400": { - "$type": "color", - "$value": "#aeaef5" - }, - "500": { - "$type": "color", - "$value": "#5555e0" - }, - "600": { - "$type": "color", - "$value": "#36366b" - } - }, - "Lavender": { - "100": { - "$type": "color", - "$value": "#f6f3fc" - }, - "200": { - "$type": "color", - "$value": "#ebe3fa" - }, - "300": { - "$type": "color", - "$value": "#e4daf7" - }, - "400": { - "$type": "color", - "$value": "#c1aaf0" - }, - "500": { - "$type": "color", - "$value": "#8153db" - }, - "600": { - "$type": "color", - "$value": "#462f75" - } - }, - "Lilac": { - "100": { - "$type": "color", - "$value": "#f7f0fa" - }, - "200": { - "$type": "color", - "$value": "#f0e1f7" - }, - "300": { - "$type": "color", - "$value": "#edd7f7" - }, - "400": { - "$type": "color", - "$value": "#d3a9e8" - }, - "500": { - "$type": "color", - "$value": "#9e4cc7" - }, - "600": { - "$type": "color", - "$value": "#562d6b" - } - }, - "Mallow": { - "100": { - "$type": "color", - "$value": "#faf0fa" - }, - "200": { - "$type": "color", - "$value": "#f5e1f4" - }, - "300": { - "$type": "color", - "$value": "#f5d7f4" - }, - "400": { - "$type": "color", - "$value": "#dea4dc" - }, - "500": { - "$type": "color", - "$value": "#b240af" - }, - "600": { - "$type": "color", - "$value": "#632861" - } - }, - "Camellia": { - "100": { - "$type": "color", - "$value": "#f9eff3" - }, - "200": { - "$type": "color", - "$value": "#f7e1eb" - }, - "300": { - "$type": "color", - "$value": "#f7d7e5" - }, - "400": { - "$type": "color", - "$value": "#e5a3c0" - }, - "500": { - "$type": "color", - "$value": "#c24279" - }, - "600": { - "$type": "color", - "$value": "#6e2343" - } - }, - "Smoke": { - "100": { - "$type": "color", - "$value": "#f5f5f5" - }, - "200": { - "$type": "color", - "$value": "#e8e8e8" - }, - "300": { - "$type": "color", - "$value": "#dedede" - }, - "400": { - "$type": "color", - "$value": "#b8b8b8" - }, - "500": { - "$type": "color", - "$value": "#6e6e6e" - }, - "600": { - "$type": "color", - "$value": "#404040" - } - }, - "Iron": { - "100": { - "$type": "color", - "$value": "#f2f4f7" - }, - "200": { - "$type": "color", - "$value": "#e6e9f0" - }, - "300": { - "$type": "color", - "$value": "#dadee5" - }, - "400": { - "$type": "color", - "$value": "#b0b5bf" - }, - "500": { - "$type": "color", - "$value": "#666f80" - }, - "600": { - "$type": "color", - "$value": "#394152" - } - } - }, - "Spacing": { - "0": { - "$type": "dimension", - "$value": "0px" - }, - "100": { - "$type": "dimension", - "$value": "4px" - }, - "200": { - "$type": "dimension", - "$value": "6px" - }, - "300": { - "$type": "dimension", - "$value": "8px" - }, - "400": { - "$type": "dimension", - "$value": "12px" - }, - "500": { - "$type": "dimension", - "$value": "16px" - }, - "600": { - "$type": "dimension", - "$value": "20px" - }, - "1000": { - "$type": "dimension", - "$value": "1000px" - } - }, - "Border-Radius": { - "0": { - "$type": "dimension", - "$value": "0px" - }, - "100": { - "$type": "dimension", - "$value": "4px" - }, - "200": { - "$type": "dimension", - "$value": "6px" - }, - "300": { - "$type": "dimension", - "$value": "8px" - }, - "400": { - "$type": "dimension", - "$value": "12px" - }, - "500": { - "$type": "dimension", - "$value": "16px" - }, - "600": { - "$type": "dimension", - "$value": "20px" - }, - "1000": { - "$type": "dimension", - "$value": "1000px" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json deleted file mode 100644 index 99d266c008..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json +++ /dev/null @@ -1,1039 +0,0 @@ -{ - "Text": { - "primary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "inverse": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "on-fill": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "theme": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "action": { - "$type": "color", - "$value": "{Blue.500}" - }, - "action-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-hover": { - "$type": "color", - "$value": "{Red.400}" - }, - "purple": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Icon": { - "primary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "purple-thick": { - "$type": "color", - "$value": "#ffffff" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "#ffffff" - } - }, - "Border": { - "grey-primary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "grey-primary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "grey-secondary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "grey-secondary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "grey-tertiary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "grey-tertiary-hover": { - "$type": "color", - "$value": "{Neutral.700}" - }, - "grey-quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "grey-quaternary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.400}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Fill": { - "primary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "primary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "secondary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary-hover": { - "$type": "color", - "$value": "{Neutral.500}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "quaternary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "primary-alpha-5": { - "$type": "color", - "$value": "{Neutral.alpha-grey-100-05}", - "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." - }, - "primary-alpha-5-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-100-10}" - }, - "primary-alpha-80": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-80}" - }, - "primary-alpha-80-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-70}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "white-alpha": { - "$type": "color", - "$value": "{Neutral.alpha-white-20}" - }, - "white-alpha-hover": { - "$type": "color", - "$value": "{Neutral.alpha-white-30}" - }, - "black": { - "$type": "color", - "$value": "{Neutral.black}" - }, - "theme-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "theme-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.400}" - }, - "theme-select": { - "$type": "color", - "$value": "{Blue.alpha-blue-500-15}" - }, - "info-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "info-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-light": { - "$type": "color", - "$value": "{Green.100}" - }, - "success-light-hover": { - "$type": "color", - "$value": "{Green.200}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-light": { - "$type": "color", - "$value": "{Orange.100}" - }, - "warning-light-hover": { - "$type": "color", - "$value": "{Orange.200}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-light": { - "$type": "color", - "$value": "{Red.100}" - }, - "error-light-hover": { - "$type": "color", - "$value": "{Red.200}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-select": { - "$type": "color", - "$value": "{Red.alpha-red-500-10}" - }, - "purple-light": { - "$type": "color", - "$value": "{Purple.100}" - }, - "purple-light-hover": { - "$type": "color", - "$value": "{Purple.200}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - } - }, - "Surface": { - "primary": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "overlay": { - "$type": "color", - "$value": "{Neutral.alpha-black-60}" - } - }, - "Background": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.700}" - } - }, - "Badge_Color": { - "Rose": { - "rose-light-1": { - "$type": "color", - "$value": "#fcf2f2" - }, - "rose-light-2": { - "$type": "color", - "$value": "#fae3e3" - }, - "rose-light-3": { - "$type": "color", - "$value": "#fad9d9" - }, - "rose-thick-1": { - "$type": "color", - "$value": "#edadad" - }, - "rose-thick-2": { - "$type": "color", - "$value": "#cc4e4e" - }, - "rose-thick-3": { - "$type": "color", - "$value": "#702828" - } - }, - "Papaya": { - "papaya-light-1": { - "$type": "color", - "$value": "#fcf4f0" - }, - "papaya-light-2": { - "$type": "color", - "$value": "#fae8de" - }, - "papaya-light-3": { - "$type": "color", - "$value": "#fadfd2" - }, - "papaya-thick-1": { - "$type": "color", - "$value": "#f0bda3" - }, - "papaya-thick-2": { - "$type": "color", - "$value": "#d67240" - }, - "papaya-thick-3": { - "$type": "color", - "$value": "#6b3215" - } - }, - "Tangerine": { - "tangerine-light-1": { - "$type": "color", - "$value": "#fff7ed" - }, - "tangerine-light-2": { - "$type": "color", - "$value": "#fcedd9" - }, - "tangerine-light-3": { - "$type": "color", - "$value": "#fae5ca" - }, - "tangerine-thick-1": { - "$type": "color", - "$value": "#f2cb99" - }, - "tangerine-thick-2": { - "$type": "color", - "$value": "#db8f2c" - }, - "tangerine-thick-3": { - "$type": "color", - "$value": "#613b0a" - } - }, - "Mango": { - "mango-light-1": { - "$type": "color", - "$value": "#fff9ec" - }, - "mango-light-2": { - "$type": "color", - "$value": "#fcf1d7" - }, - "mango-light-3": { - "$type": "color", - "$value": "#fae9c3" - }, - "mango-thick-1": { - "$type": "color", - "$value": "#f5d68e" - }, - "mango-thick-2": { - "$type": "color", - "$value": "#e0a416" - }, - "mango-thick-3": { - "$type": "color", - "$value": "#5c4102" - } - }, - "Lemon": { - "lemon-light-1": { - "$type": "color", - "$value": "#fffbe8" - }, - "lemon-light-2": { - "$type": "color", - "$value": "#fcf5cf" - }, - "lemon-light-3": { - "$type": "color", - "$value": "#faefb9" - }, - "lemon-thick-1": { - "$type": "color", - "$value": "#f5e282" - }, - "lemon-thick-2": { - "$type": "color", - "$value": "#e0bb00" - }, - "lemon-thick-3": { - "$type": "color", - "$value": "#574800" - } - }, - "Olive": { - "olive-light-1": { - "$type": "color", - "$value": "#f9fae6" - }, - "olive-light-2": { - "$type": "color", - "$value": "#f6f7d0" - }, - "olive-light-3": { - "$type": "color", - "$value": "#f0f2b3" - }, - "olive-thick-1": { - "$type": "color", - "$value": "#dbde83" - }, - "olive-thick-2": { - "$type": "color", - "$value": "#adb204" - }, - "olive-thick-3": { - "$type": "color", - "$value": "#4a4c03" - } - }, - "Lime": { - "lime-light-1": { - "$type": "color", - "$value": "#f6f9e6" - }, - "lime-light-2": { - "$type": "color", - "$value": "#eef5ce" - }, - "lime-light-3": { - "$type": "color", - "$value": "#e7f0bb" - }, - "lime-thick-1": { - "$type": "color", - "$value": "#cfdb91" - }, - "lime-thick-2": { - "$type": "color", - "$value": "#92a822" - }, - "lime-thick-3": { - "$type": "color", - "$value": "#414d05" - } - }, - "Grass": { - "grass-light-1": { - "$type": "color", - "$value": "#f4faeb" - }, - "grass-light-2": { - "$type": "color", - "$value": "#e9f5d7" - }, - "grass-light-3": { - "$type": "color", - "$value": "#def0c5" - }, - "grass-thick-1": { - "$type": "color", - "$value": "#bfd998" - }, - "grass-thick-2": { - "$type": "color", - "$value": "#75a828" - }, - "grass-thick-3": { - "$type": "color", - "$value": "#334d0c" - } - }, - "Forest": { - "forest-light-1": { - "$type": "color", - "$value": "#f1faf0" - }, - "forest-light-2": { - "$type": "color", - "$value": "#e2f5df" - }, - "forest-light-3": { - "$type": "color", - "$value": "#d7f0d3" - }, - "forest-thick-1": { - "$type": "color", - "$value": "#a8d6a1" - }, - "forest-thick-2": { - "$type": "color", - "$value": "#49a33b" - }, - "forest-thick-3": { - "$type": "color", - "$value": "#1e4f16" - } - }, - "Jade": { - "jade-light-1": { - "$type": "color", - "$value": "#f0faf6" - }, - "jade-light-2": { - "$type": "color", - "$value": "#dff5eb" - }, - "jade-light-3": { - "$type": "color", - "$value": "#cef0e1" - }, - "jade-thick-1": { - "$type": "color", - "$value": "#90d1b5" - }, - "jade-thick-2": { - "$type": "color", - "$value": "#1c9963" - }, - "jade-thick-3": { - "$type": "color", - "$value": "#075231" - } - }, - "Aqua": { - "aqua-light-1": { - "$type": "color", - "$value": "#f0f9fa" - }, - "aqua-light-2": { - "$type": "color", - "$value": "#dff3f5" - }, - "aqua-light-3": { - "$type": "color", - "$value": "#ccecf0" - }, - "aqua-thick-1": { - "$type": "color", - "$value": "#83ccd4" - }, - "aqua-thick-2": { - "$type": "color", - "$value": "#008e9e" - }, - "aqua-thick-3": { - "$type": "color", - "$value": "#004e57" - } - }, - "Azure": { - "azure-light-1": { - "$type": "color", - "$value": "#f0f6fa" - }, - "azure-light-2": { - "$type": "color", - "$value": "#e1eef7" - }, - "azure-light-3": { - "$type": "color", - "$value": "#d3e6f5" - }, - "azure-thick-1": { - "$type": "color", - "$value": "#88c0eb" - }, - "azure-thick-2": { - "$type": "color", - "$value": "#0877cc" - }, - "azure-thick-3": { - "$type": "color", - "$value": "#154469" - } - }, - "Denim": { - "denim-light-1": { - "$type": "color", - "$value": "#f0f3fa" - }, - "denim-light-2": { - "$type": "color", - "$value": "#e3ebfa" - }, - "denim-light-3": { - "$type": "color", - "$value": "#d7e2f7" - }, - "denim-thick-1": { - "$type": "color", - "$value": "#9ab6ed" - }, - "denim-thick-2": { - "$type": "color", - "$value": "#3267d1" - }, - "denim-thick-3": { - "$type": "color", - "$value": "#223c70" - } - }, - "Mauve": { - "mauve-light-1": { - "$type": "color", - "$value": "#f2f2fc" - }, - "mauve-thick-2": { - "$type": "color", - "$value": "#5555e0" - }, - "mauve-thick-3": { - "$type": "color", - "$value": "#36366b" - }, - "mauve-thick-1": { - "$type": "color", - "$value": "#aeaef5" - } - }, - "Lavender": { - "lavender-light-1": { - "$type": "color", - "$value": "#f6f3fc" - }, - "lavender-light-2": { - "$type": "color", - "$value": "#ebe3fa" - }, - "lavender-light-3": { - "$type": "color", - "$value": "#e4daf7" - }, - "lavender-thick-1": { - "$type": "color", - "$value": "#c1aaf0" - }, - "lavender-thick-2": { - "$type": "color", - "$value": "#8153db" - }, - "lavender-thick-3": { - "$type": "color", - "$value": "#462f75" - } - }, - "Lilac": { - "liliac-light-1": { - "$type": "color", - "$value": "#f7f0fa" - }, - "liliac-light-2": { - "$type": "color", - "$value": "#f0e1f7" - }, - "liliac-light-3": { - "$type": "color", - "$value": "#edd7f7" - }, - "liliac-thick-1": { - "$type": "color", - "$value": "#d3a9e8" - }, - "liliac-thick-2": { - "$type": "color", - "$value": "#9e4cc7" - }, - "liliac-thick-3": { - "$type": "color", - "$value": "#562d6b" - } - }, - "Mallow": { - "mallow-light-1": { - "$type": "color", - "$value": "#faf0fa" - }, - "mallow-light-2": { - "$type": "color", - "$value": "#f5e1f4" - }, - "mallow-light-3": { - "$type": "color", - "$value": "#f5d7f4" - }, - "mallow-thick-1": { - "$type": "color", - "$value": "#dea4dc" - }, - "mallow-thick-2": { - "$type": "color", - "$value": "#b240af" - }, - "mallow-thick-3": { - "$type": "color", - "$value": "#632861" - } - }, - "Camellia": { - "camellia-light-1": { - "$type": "color", - "$value": "#f9eff3" - }, - "camellia-light-2": { - "$type": "color", - "$value": "#f7e1eb" - }, - "camellia-light-3": { - "$type": "color", - "$value": "#f7d7e5" - }, - "camellia-thick-1": { - "$type": "color", - "$value": "#e5a3c0" - }, - "camellia-thick-2": { - "$type": "color", - "$value": "#c24279" - }, - "camellia-thick-3": { - "$type": "color", - "$value": "#6e2343" - } - }, - "Smoke": { - "smoke-light-1": { - "$type": "color", - "$value": "#f5f5f5" - }, - "smoke-light-2": { - "$type": "color", - "$value": "#e8e8e8" - }, - "smoke-light-3": { - "$type": "color", - "$value": "#dedede" - }, - "smoke-thick-1": { - "$type": "color", - "$value": "#b8b8b8" - }, - "smoke-thick-2": { - "$type": "color", - "$value": "#6e6e6e" - }, - "smoke-thick-3": { - "$type": "color", - "$value": "#404040" - } - }, - "Iron": { - "icon-light-1": { - "$type": "color", - "$value": "#f2f4f7" - }, - "icon-light-2": { - "$type": "color", - "$value": "#e6e9f0" - }, - "icon-light-3": { - "$type": "color", - "$value": "#dadee5" - }, - "icon-thick-1": { - "$type": "color", - "$value": "#b0b5bf" - }, - "icon-thick-2": { - "$type": "color", - "$value": "#666f80" - }, - "icon-thick-3": { - "$type": "color", - "$value": "#394152" - } - } - }, - "Shadow": { - "sm": { - "$type": "dimension", - "$value": "0px" - }, - "md": { - "$type": "dimension", - "$value": "0px" - } - }, - "Brand": { - "Skyline": { - "$type": "color", - "$value": "#00b5ff" - }, - "Aqua": { - "$type": "color", - "$value": "#00c8ff" - }, - "Violet": { - "$type": "color", - "$value": "#9327ff" - }, - "Amethyst": { - "$type": "color", - "$value": "#8427e0" - }, - "Berry": { - "$type": "color", - "$value": "#e3006d" - }, - "Coral": { - "$type": "color", - "$value": "#fb006d" - }, - "Golden": { - "$type": "color", - "$value": "#f7931e" - }, - "Amber": { - "$type": "color", - "$value": "#ffbd00" - }, - "Lemon": { - "$type": "color", - "$value": "#ffce00" - } - }, - "Other_Colors": { - "text-highlight": { - "$type": "color", - "$value": "{Blue.200}" - } - }, - "Spacing": { - "spacing-0": { - "$type": "dimension", - "$value": "{Spacing.0}" - }, - "spacing-xs": { - "$type": "dimension", - "$value": "{Spacing.100}" - }, - "spacing-s": { - "$type": "dimension", - "$value": "{Spacing.200}" - }, - "spacing-m": { - "$type": "dimension", - "$value": "{Spacing.300}" - }, - "spacing-l": { - "$type": "dimension", - "$value": "{Spacing.400}" - }, - "spacing-xl": { - "$type": "dimension", - "$value": "{Spacing.500}" - }, - "spacing-xxl": { - "$type": "dimension", - "$value": "{Spacing.600}" - }, - "spacing-full": { - "$type": "dimension", - "$value": "{Spacing.1000}" - } - }, - "Border_Radius": { - "border-radius-0": { - "$type": "dimension", - "$value": "{Border-Radius.0}" - }, - "border-radius-xs": { - "$type": "dimension", - "$value": "{Border-Radius.100}" - }, - "border-radius-s": { - "$type": "dimension", - "$value": "{Border-Radius.200}" - }, - "border-radius-m": { - "$type": "dimension", - "$value": "{Border-Radius.300}" - }, - "border-radius-l": { - "$type": "dimension", - "$value": "{Border-Radius.400}" - }, - "border-radius-xl": { - "$type": "dimension", - "$value": "{Border-Radius.500}" - }, - "border-radius-xxl": { - "$type": "dimension", - "$value": "{Border-Radius.600}" - }, - "border-radius-full": { - "$type": "dimension", - "$value": "{Border-Radius.1000}" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json deleted file mode 100644 index 4e6b0543dc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json +++ /dev/null @@ -1,1039 +0,0 @@ -{ - "Text": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "inverse": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "on-fill": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "theme": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "action": { - "$type": "color", - "$value": "{Blue.500}" - }, - "action-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "purple": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Icon": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Border": { - "grey-primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "grey-primary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "grey-secondary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "grey-secondary-hover": { - "$type": "color", - "$value": "{Neutral.700}" - }, - "grey-tertiary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "grey-tertiary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "grey-quaternary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "grey-quaternary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Fill": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "primary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "secondary-hover": { - "$type": "color", - "$value": "{Neutral.500}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "tertiary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "quaternary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "primary-alpha-5": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-05}", - "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." - }, - "primary-alpha-5-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-10}" - }, - "primary-alpha-80": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-80}" - }, - "primary-alpha-80-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-70}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "white-alpha": { - "$type": "color", - "$value": "{Neutral.alpha-white-20}" - }, - "white-alpha-hover": { - "$type": "color", - "$value": "{Neutral.alpha-white-30}" - }, - "black": { - "$type": "color", - "$value": "{Neutral.black}" - }, - "theme-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "theme-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "theme-select": { - "$type": "color", - "$value": "{Blue.alpha-blue-500-15}" - }, - "info-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "info-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-light": { - "$type": "color", - "$value": "{Green.100}" - }, - "success-light-hover": { - "$type": "color", - "$value": "{Green.200}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-light": { - "$type": "color", - "$value": "{Orange.100}" - }, - "warning-light-hover": { - "$type": "color", - "$value": "{Orange.200}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-light": { - "$type": "color", - "$value": "{Red.100}" - }, - "error-light-hover": { - "$type": "color", - "$value": "{Red.200}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "error-select": { - "$type": "color", - "$value": "{Red.alpha-red-500-10}" - }, - "purple-light": { - "$type": "color", - "$value": "{Purple.100}" - }, - "purple-light-hover": { - "$type": "color", - "$value": "{Purple.200}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - } - }, - "Surface": { - "primary": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "overlay": { - "$type": "color", - "$value": "{Neutral.alpha-black-60}" - } - }, - "Background": { - "primary": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.300}" - } - }, - "Badge_Color": { - "Rose": { - "rose-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Rose.100}" - }, - "rose-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Rose.200}" - }, - "rose-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Rose.300}" - }, - "rose-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Rose.400}" - }, - "rose-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Rose.500}" - }, - "rose-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Rose.600}" - } - }, - "Papaya": { - "papaya-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.100}" - }, - "papaya-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.200}" - }, - "papaya-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.300}" - }, - "papaya-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.400}" - }, - "papaya-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.500}" - }, - "papaya-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.600}" - } - }, - "Tangerine": { - "tangerine-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.100}" - }, - "tangerine-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.200}" - }, - "tangerine-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.300}" - }, - "tangerine-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.400}" - }, - "tangerine-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.500}" - }, - "tangerine-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.600}" - } - }, - "Mango": { - "mango-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mango.100}" - }, - "mango-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Mango.200}" - }, - "mango-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Mango.300}" - }, - "mango-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mango.400}" - }, - "mango-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mango.500}" - }, - "mango-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mango.600}" - } - }, - "Lemon": { - "lemon-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.100}" - }, - "lemon-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.200}" - }, - "lemon-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.300}" - }, - "lemon-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.400}" - }, - "lemon-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.500}" - }, - "lemon-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.600}" - } - }, - "Olive": { - "olive-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Olive.100}" - }, - "olive-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Olive.200}" - }, - "olive-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Olive.300}" - }, - "olive-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Olive.400}" - }, - "olive-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Olive.500}" - }, - "olive-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Olive.600}" - } - }, - "Lime": { - "lime-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lime.100}" - }, - "lime-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lime.200}" - }, - "lime-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lime.300}" - }, - "lime-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lime.400}" - }, - "lime-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lime.500}" - }, - "lime-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lime.600}" - } - }, - "Grass": { - "grass-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Grass.100}" - }, - "grass-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Grass.200}" - }, - "grass-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Grass.300}" - }, - "grass-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Grass.400}" - }, - "grass-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Grass.500}" - }, - "grass-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Grass.600}" - } - }, - "Forest": { - "forest-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Forest.100}" - }, - "forest-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Forest.200}" - }, - "forest-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Forest.300}" - }, - "forest-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Forest.400}" - }, - "forest-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Forest.500}" - }, - "forest-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Forest.600}" - } - }, - "Jade": { - "jade-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Jade.100}" - }, - "jade-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Jade.200}" - }, - "jade-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Jade.300}" - }, - "jade-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Jade.400}" - }, - "jade-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Jade.500}" - }, - "jade-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Jade.600}" - } - }, - "Aqua": { - "aqua-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.100}" - }, - "aqua-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.200}" - }, - "aqua-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.300}" - }, - "aqua-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.400}" - }, - "aqua-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.500}" - }, - "aqua-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.600}" - } - }, - "Azure": { - "azure-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Azure.100}" - }, - "azure-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Azure.200}" - }, - "azure-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Azure.300}" - }, - "azure-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Azure.400}" - }, - "azure-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Azure.500}" - }, - "azure-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Azure.600}" - } - }, - "Denim": { - "denim-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Denim.100}" - }, - "denim-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Denim.200}" - }, - "denim-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Denim.300}" - }, - "denim-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Denim.400}" - }, - "denim-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Denim.500}" - }, - "denim-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Denim.600}" - } - }, - "Mauve": { - "mauve-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.100}" - }, - "mauve-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.500}" - }, - "mauve-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.600}" - }, - "mauve-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.400}" - } - }, - "Lavender": { - "lavender-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.100}" - }, - "lavender-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.200}" - }, - "lavender-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.300}" - }, - "lavender-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.400}" - }, - "lavender-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.500}" - }, - "lavender-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.600}" - } - }, - "Lilac": { - "liliac-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.100}" - }, - "liliac-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.200}" - }, - "liliac-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.300}" - }, - "liliac-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.400}" - }, - "liliac-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.500}" - }, - "liliac-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.600}" - } - }, - "Mallow": { - "mallow-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.100}" - }, - "mallow-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.200}" - }, - "mallow-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.300}" - }, - "mallow-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.400}" - }, - "mallow-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.500}" - }, - "mallow-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.600}" - } - }, - "Camellia": { - "camellia-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.100}" - }, - "camellia-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.200}" - }, - "camellia-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.300}" - }, - "camellia-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.400}" - }, - "camellia-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.500}" - }, - "camellia-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.600}" - } - }, - "Smoke": { - "smoke-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.100}" - }, - "smoke-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.200}" - }, - "smoke-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.300}" - }, - "smoke-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.400}" - }, - "smoke-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.500}" - }, - "smoke-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.600}" - } - }, - "Iron": { - "icon-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Iron.100}" - }, - "icon-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Iron.200}" - }, - "icon-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Iron.300}" - }, - "icon-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Iron.400}" - }, - "icon-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Iron.500}" - }, - "icon-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Iron.600}" - } - } - }, - "Shadow": { - "sm": { - "$type": "dimension", - "$value": "0px" - }, - "md": { - "$type": "dimension", - "$value": "0px" - } - }, - "Brand": { - "Skyline": { - "$type": "color", - "$value": "#00b5ff" - }, - "Aqua": { - "$type": "color", - "$value": "#00c8ff" - }, - "Violet": { - "$type": "color", - "$value": "#9327ff" - }, - "Amethyst": { - "$type": "color", - "$value": "#8427e0" - }, - "Berry": { - "$type": "color", - "$value": "#e3006d" - }, - "Coral": { - "$type": "color", - "$value": "#fb006d" - }, - "Golden": { - "$type": "color", - "$value": "#f7931e" - }, - "Amber": { - "$type": "color", - "$value": "#ffbd00" - }, - "Lemon": { - "$type": "color", - "$value": "#ffce00" - } - }, - "Other_Colors": { - "text-highlight": { - "$type": "color", - "$value": "{Blue.200}" - } - }, - "Spacing": { - "spacing-0": { - "$type": "dimension", - "$value": "{Spacing.0}" - }, - "spacing-xs": { - "$type": "dimension", - "$value": "{Spacing.100}" - }, - "spacing-s": { - "$type": "dimension", - "$value": "{Spacing.200}" - }, - "spacing-m": { - "$type": "dimension", - "$value": "{Spacing.300}" - }, - "spacing-l": { - "$type": "dimension", - "$value": "{Spacing.400}" - }, - "spacing-xl": { - "$type": "dimension", - "$value": "{Spacing.500}" - }, - "spacing-xxl": { - "$type": "dimension", - "$value": "{Spacing.600}" - }, - "spacing-full": { - "$type": "dimension", - "$value": "{Spacing.1000}" - } - }, - "Border_Radius": { - "border-radius-0": { - "$type": "dimension", - "$value": "{Border-Radius.0}" - }, - "border-radius-xs": { - "$type": "dimension", - "$value": "{Border-Radius.100}" - }, - "border-radius-s": { - "$type": "dimension", - "$value": "{Border-Radius.200}" - }, - "border-radius-m": { - "$type": "dimension", - "$value": "{Border-Radius.300}" - }, - "border-radius-l": { - "$type": "dimension", - "$value": "{Border-Radius.400}" - }, - "border-radius-xl": { - "$type": "dimension", - "$value": "{Border-Radius.500}" - }, - "border-radius-xxl": { - "$type": "dimension", - "$value": "{Border-Radius.600}" - }, - "border-radius-full": { - "$type": "dimension", - "$value": "{Border-Radius.1000}" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart deleted file mode 100644 index bddcdb4eae..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ /dev/null @@ -1,300 +0,0 @@ -// ignore_for_file: avoid_print, depend_on_referenced_packages - -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; - -void main() { - generatePrimitive(); - generateSemantic(); -} - -void generatePrimitive() { - // 1. Load the JSON file. - final jsonString = - File('script/Primitive.Mode 1.tokens.json').readAsStringSync(); - final jsonData = jsonDecode(jsonString) as Map; - - // 2. Prepare the output code. - final buffer = StringBuffer(); - - buffer.writeln(''' -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: ${DateTime.now().toIso8601String()} -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:flutter/material.dart'; - -class AppFlowyPrimitiveTokens { - AppFlowyPrimitiveTokens._();'''); - - // 3. Process each color category. - jsonData.forEach((categoryName, categoryData) { - categoryData.forEach((tokenName, tokenData) { - processPrimitiveTokenData( - buffer, - tokenData, - '${categoryName}_$tokenName', - ); - }); - }); - - buffer.writeln('}'); - - // 4. Write the output to a Dart file. - final outputFile = File('lib/src/theme/data/appflowy_default/primitive.dart'); - outputFile.writeAsStringSync(buffer.toString()); - - print('Successfully generated ${outputFile.path}'); -} - -void processPrimitiveTokenData( - StringBuffer buffer, - Map tokenData, - final String currentTokenName, -) { - if (tokenData - case { - r'$type': 'color', - r'$value': final String colorValue, - }) { - final dartColorValue = convertColor(colorValue); - final dartTokenName = currentTokenName.replaceAll('-', '_').toCamelCase(); - - buffer.writeln(''' - - /// $colorValue - static Color get $dartTokenName => Color(0x$dartColorValue);'''); - } else { - tokenData.forEach((key, value) { - if (value is Map) { - processPrimitiveTokenData(buffer, value, '${currentTokenName}_$key'); - } - }); - } -} - -void generateSemantic() { - // 1. Load the JSON file. - final lightJsonString = - File('script/Semantic.Light Mode.tokens.json').readAsStringSync(); - final darkJsonString = - File('script/Semantic.Dark Mode.tokens.json').readAsStringSync(); - final lightJsonData = jsonDecode(lightJsonString) as Map; - final darkJsonData = jsonDecode(darkJsonString) as Map; - - // 2. Prepare the output code. - final buffer = StringBuffer(); - - buffer.writeln(''' -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: ${DateTime.now().toIso8601String()} -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import '../shared.dart'; -import 'primitive.dart'; - -class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); - - // 3. Process light mode semantic tokens - buffer.writeln(''' - @override - AppFlowyThemeData light() { - final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);'''); - - lightJsonData.forEach((categoryName, categoryData) { - if ([ - 'Spacing', - 'Border_Radius', - 'Shadow', - 'Badge_Color', - ].contains(categoryName)) { - return; - } - - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); - - categoryData.forEach((tokenName, tokenData) { - processSemanticTokenData(buffer, tokenData, tokenName); - }); - buffer.writeln(' );'); - }); - - buffer.writeln(); - buffer.writeln(''' - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - }'''); - - buffer.writeln(); - - buffer.writeln(''' - @override - AppFlowyThemeData dark() { - final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);'''); - - darkJsonData.forEach((categoryName, categoryData) { - if ([ - 'Spacing', - 'Border_Radius', - 'Shadow', - 'Badge_Color', - ].contains(categoryName)) { - return; - } - - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); - - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map) { - processSemanticTokenData(buffer, tokenData, tokenName); - } - }); - buffer.writeln(' );'); - }); - - buffer.writeln(); - - buffer.writeln(''' - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - }'''); - - buffer.writeln('}'); - - // 4. Write the output to a Dart file. - final outputFile = File('lib/src/theme/data/appflowy_default/semantic.dart'); - outputFile.writeAsStringSync(buffer.toString()); - - print('Successfully generated ${outputFile.path}'); -} - -void processSemanticTokenData( - StringBuffer buffer, - Map json, - final String currentTokenName, -) { - if (json - case { - r'$type': 'color', - r'$value': final String value, - }) { - final semanticTokenName = - currentTokenName.replaceAll('-', '_').toCamelCase(); - - final String colorValueOrPrimitiveToken; - if (value.isColor) { - colorValueOrPrimitiveToken = 'Color(0x${convertColor(value)})'; - } else { - final primitiveToken = value - .replaceAll(RegExp(r'\{|\}'), '') - .replaceAll(RegExp(r'\.|-'), '_') - .toCamelCase(); - colorValueOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; - } - - buffer.writeln(' $semanticTokenName: $colorValueOrPrimitiveToken,'); - } else { - json.forEach((key, value) { - if (value is Map) { - processSemanticTokenData( - buffer, - value, - '${currentTokenName}_$key', - ); - } - }); - } -} - -String convertColor(String hexColor) { - String color = hexColor.toUpperCase().replaceAll('#', ''); - if (color.length == 6) { - color = 'FF$color'; // Add missing alpha channel - } else if (color.length == 8) { - color = color.substring(6) + color.substring(0, 6); // Rearrange to ARGB - } - return color; -} - -extension on String { - String toCamelCase() { - return split('_').mapIndexed((index, part) { - if (index == 0) { - return part.toLowerCase(); - } else { - return part[0].toUpperCase() + part.substring(1).toLowerCase(); - } - }).join(); - } - - String toCapitalize() { - if (isEmpty) { - return this; - } - return '${this[0].toUpperCase()}${substring(1)}'; - } - - bool get isColor => - startsWith('#') || - (startsWith('0x') && length == 10) || - (startsWith('0xFF') && length == 12); -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 4178edd294..9cd3a06313 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -90,7 +90,6 @@ class FlowyColorScheme { required this.scrollbarColor, required this.scrollbarHoverColor, required this.lightIconColor, - required this.toolbarHoverColor, }); final Color surface; @@ -155,7 +154,6 @@ class FlowyColorScheme { final Color scrollbarHoverColor; final Color lightIconColor; - final Color toolbarHoverColor; factory FlowyColorScheme.fromJson(Map json) => _$FlowyColorSchemeFromJson(json); diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index 8d49b8dfa1..2aa455b404 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -86,7 +86,6 @@ class DandelionColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const DandelionColorScheme.dark() @@ -145,6 +144,5 @@ class DandelionColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 0e39de8fa8..f829d3a67e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -83,7 +83,6 @@ class DefaultColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const DefaultColorScheme.dark() @@ -142,6 +141,5 @@ class DefaultColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: ColorSchemeConstants.lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 590d26db3e..97ff5221de 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -82,7 +82,6 @@ class LavenderColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const LavenderColorScheme.dark() @@ -141,6 +140,5 @@ class LavenderColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x40FFFFFF), scrollbarHoverColor: const Color(0x80FFFFFF), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 3f39ae4c84..5d9ff8b97c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -88,64 +88,63 @@ class LemonadeColorScheme extends FlowyColorScheme { scrollbarColor: const Color(0x3F171717), scrollbarHoverColor: const Color(0x7F171717), lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const LemonadeColorScheme.dark() : super( - surface: const Color(0xff292929), - hover: const Color(0xff1f1f1f), - selector: _darkShader2, - red: const Color(0xfffb006d), - yellow: const Color(0xffffd667), - green: const Color(0xff66cf80), - shader1: _white, - shader2: _darkShader2, - shader3: const Color(0xff828282), - shader4: const Color(0xffbdbdbd), - shader5: _darkShader5, - shader6: _darkShader6, - shader7: _white, - bg1: const Color(0xFFD5A200), - bg2: _black, - bg3: _darkMain1, - bg4: const Color(0xff2c144b), - tint1: const Color(0x4d9327FF), - tint2: const Color(0x66FC0088), - tint3: const Color(0x4dFC00E2), - tint4: const Color(0x80BE5B00), - tint5: const Color(0x33F8EE00), - tint6: const Color(0x4d6DC300), - tint7: const Color(0x5900BD2A), - tint8: const Color(0x80008890), - tint9: const Color(0x4d0029FF), - main1: _darkMain1, - main2: _darkMain1, - shadow: _black, - sidebarBg: const Color(0xff232B38), - divider: _darkShader3, - topbarBg: _darkShader1, - icon: _darkShader5, - text: _darkShader5, - secondaryText: _darkShader5, - strongText: Colors.white, - input: _darkInput, - hint: _darkShader5, - primary: _darkMain1, - onPrimary: _darkShader1, - hoverBG1: _darkMain1, - hoverBG2: _darkMain1, - hoverBG3: _darkShader3, - hoverFG: _darkShader1, - questionBubbleBG: _darkShader3, - progressBarBGColor: _darkShader3, - toolbarColor: _darkInput, - toggleButtonBGColor: _darkShader1, - calendarWeekendBGColor: const Color(0xff121212), - gridRowCountColor: _darkMain1, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6); + surface: const Color(0xff292929), + hover: const Color(0xff1f1f1f), + selector: _darkShader2, + red: const Color(0xfffb006d), + yellow: const Color(0xffffd667), + green: const Color(0xff66cf80), + shader1: _white, + shader2: _darkShader2, + shader3: const Color(0xff828282), + shader4: const Color(0xffbdbdbd), + shader5: _darkShader5, + shader6: _darkShader6, + shader7: _white, + bg1: const Color(0xFFD5A200), + bg2: _black, + bg3: _darkMain1, + bg4: const Color(0xff2c144b), + tint1: const Color(0x4d9327FF), + tint2: const Color(0x66FC0088), + tint3: const Color(0x4dFC00E2), + tint4: const Color(0x80BE5B00), + tint5: const Color(0x33F8EE00), + tint6: const Color(0x4d6DC300), + tint7: const Color(0x5900BD2A), + tint8: const Color(0x80008890), + tint9: const Color(0x4d0029FF), + main1: _darkMain1, + main2: _darkMain1, + shadow: _black, + sidebarBg: const Color(0xff232B38), + divider: _darkShader3, + topbarBg: _darkShader1, + icon: _darkShader5, + text: _darkShader5, + secondaryText: _darkShader5, + strongText: Colors.white, + input: _darkInput, + hint: _darkShader5, + primary: _darkMain1, + onPrimary: _darkShader1, + hoverBG1: _darkMain1, + hoverBG2: _darkMain1, + hoverBG3: _darkShader3, + hoverFG: _darkShader1, + questionBubbleBG: _darkShader3, + progressBarBGColor: _darkShader3, + toolbarColor: _darkInput, + toggleButtonBGColor: _darkShader1, + calendarWeekendBGColor: const Color(0xff121212), + gridRowCountColor: _darkMain1, + borderColor: ColorSchemeConstants.darkBorderColor, + scrollbarColor: const Color(0x40FFFFFF), + scrollbarHoverColor: const Color(0x80FFFFFF), + lightIconColor: const Color(0xFF8F959E), + ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 6f37058f00..0b6ff4fb3f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -48,8 +48,6 @@ String languageFromLocale(Locale locale) { default: return locale.languageCode; } - case "mr": - return "मराठी"; case "he": return "עברית"; case "hu": diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 9ce1f0323d..a2bfd16b06 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -41,7 +41,6 @@ class AFThemeExtension extends ThemeExtension { required this.borderColor, required this.scrollbarColor, required this.scrollbarHoverColor, - required this.toolbarHoverColor, required this.lightIconColor, }); @@ -87,7 +86,6 @@ class AFThemeExtension extends ThemeExtension { final Color scrollbarColor; final Color scrollbarHoverColor; - final Color toolbarHoverColor; final Color lightIconColor; @override @@ -125,7 +123,6 @@ class AFThemeExtension extends ThemeExtension { Color? scrollbarColor, Color? scrollbarHoverColor, Color? lightIconColor, - Color? toolbarHoverColor, }) => AFThemeExtension( warning: warning ?? this.warning, @@ -162,7 +159,6 @@ class AFThemeExtension extends ThemeExtension { scrollbarColor: scrollbarColor ?? this.scrollbarColor, scrollbarHoverColor: scrollbarHoverColor ?? this.scrollbarHoverColor, lightIconColor: lightIconColor ?? this.lightIconColor, - toolbarHoverColor: toolbarHoverColor ?? this.toolbarHoverColor, ); @override @@ -219,8 +215,6 @@ class AFThemeExtension extends ThemeExtension { scrollbarHoverColor: Color.lerp(scrollbarHoverColor, other.scrollbarHoverColor, t)!, lightIconColor: Color.lerp(lightIconColor, other.lightIconColor, t)!, - toolbarHoverColor: - Color.lerp(toolbarHoverColor, other.toolbarHoverColor, t)!, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 9bf0245dc0..cb5cbb9cee 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -1,5 +1,5 @@ name: flowy_infra -description: AppFlowy Infra. +description: A new Flutter package project. version: 0.0.1 homepage: https://appflowy.io @@ -15,14 +15,50 @@ dependencies: path: ^1.8.2 time: ">=2.0.0" uuid: ">=2.2.2" - bloc: ^9.0.0 + bloc: ^8.1.2 freezed_annotation: ^2.1.0 file_picker: ^8.0.2 file: ^7.0.0 - analyzer: 6.11.0 dev_dependencies: build_runner: ^2.4.9 flutter_lints: ^3.0.1 freezed: ^2.4.7 json_serializable: ^6.5.4 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml index 4a8ad910cb..f2e3eb8749 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_platform_interface description: A new Flutter package project. version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: environment: sdk: ">=2.12.0 <3.0.0" @@ -17,3 +17,5 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 + +flutter: \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml index d4364a6400..bbdac0d2e4 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_web description: A new Flutter package project. version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: publish_to: none environment: @@ -25,4 +25,4 @@ flutter: platforms: web: pluginClass: FlowyInfraUIPlugin - fileName: flowy_infra_ui_web.dart + fileName: flowy_infra_ui_web.dart \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 6a154d4d48..190d840a41 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -4,23 +4,6 @@ import 'package:flutter/material.dart'; export 'package:appflowy_popover/appflowy_popover.dart'; -class ShadowConstants { - ShadowConstants._(); - - static const List lightSmall = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 20, color: Color(0x1A1F2329)), - ]; - static const List lightMedium = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x121F2225)), - ]; - static const List darkSmall = [ - BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x7A000000)), - ]; - static const List darkMedium = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x7A000000)), - ]; -} - class AppFlowyPopover extends StatelessWidget { const AppFlowyPopover({ super.key, @@ -42,7 +25,6 @@ class AppFlowyPopover extends StatelessWidget { this.skipTraversal = false, this.decorationColor, this.borderRadius, - this.popoverDecoration, this.animationDuration = const Duration(), this.slideDistance = 5.0, this.beginScaleFactor = 0.9, @@ -74,7 +56,6 @@ class AppFlowyPopover extends StatelessWidget { final double endScaleFactor; final double beginOpacity; final double endOpacity; - final Decoration? popoverDecoration; /// The widget that will be used to trigger the popover. /// @@ -121,7 +102,6 @@ class AppFlowyPopover extends StatelessWidget { popupBuilder: (context) => _PopoverContainer( constraints: constraints, margin: margin, - decoration: popoverDecoration, decorationColor: decorationColor, borderRadius: borderRadius, child: popupBuilder(context), @@ -136,7 +116,6 @@ class _PopoverContainer extends StatelessWidget { const _PopoverContainer({ this.decorationColor, this.borderRadius, - this.decoration, required this.child, required this.margin, required this.constraints, @@ -147,7 +126,6 @@ class _PopoverContainer extends StatelessWidget { final EdgeInsets margin; final Color? decorationColor; final BorderRadius? borderRadius; - final Decoration? decoration; @override Widget build(BuildContext context) { @@ -155,11 +133,10 @@ class _PopoverContainer extends StatelessWidget { type: MaterialType.transparency, child: Container( padding: margin, - decoration: decoration ?? - context.getPopoverDecoration( - color: decorationColor, - borderRadius: borderRadius, - ), + decoration: context.getPopoverDecoration( + color: decorationColor, + borderRadius: borderRadius, + ), constraints: constraints, child: child, ), @@ -167,7 +144,7 @@ class _PopoverContainer extends StatelessWidget { } } -extension PopoverDecoration on BuildContext { +extension on BuildContext { /// The decoration of the popover. /// /// Don't customize the entire decoration of the popover, @@ -179,9 +156,26 @@ extension PopoverDecoration on BuildContext { final borderColor = Theme.of(this).brightness == Brightness.light ? ColorSchemeConstants.lightBorderColor : ColorSchemeConstants.darkBorderColor; - final shadows = Theme.of(this).brightness == Brightness.light - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall; + final shadows = [ + const BoxShadow( + color: Color(0x0A1F2329), + blurRadius: 24, + offset: Offset(0, 8), + spreadRadius: 8, + ), + const BoxShadow( + color: Color(0x0A1F2329), + blurRadius: 12, + offset: Offset(0, 6), + spreadRadius: 0, + ), + const BoxShadow( + color: Color(0x0F1F2329), + blurRadius: 8, + offset: Offset(0, 4), + spreadRadius: -8, + ) + ]; return ShapeDecoration( color: color ?? Theme.of(this).cardColor, shape: RoundedRectangleBorder( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart deleted file mode 100644 index 96a22a6f85..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; - -class FlowyToolbarButton extends StatelessWidget { - final Widget child; - final VoidCallback? onPressed; - final EdgeInsets padding; - final String? tooltip; - - const FlowyToolbarButton({ - super.key, - this.onPressed, - this.tooltip, - this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 6), - required this.child, - }); - - @override - Widget build(BuildContext context) { - final tooltipMessage = tooltip ?? ''; - - return FlowyTooltip( - message: tooltipMessage, - padding: EdgeInsets.zero, - child: RawMaterialButton( - clipBehavior: Clip.antiAlias, - constraints: const BoxConstraints(minWidth: 36, minHeight: 32), - hoverElevation: 0, - highlightElevation: 0, - padding: EdgeInsets.zero, - shape: const RoundedRectangleBorder(borderRadius: Corners.s6Border), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, - onPressed: onPressed, - child: child, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart similarity index 88% rename from frontend/appflowy_flutter/lib/shared/error_page/error_page.dart rename to frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart index 9661fd822a..d395873bd7 100644 --- a/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -1,18 +1,14 @@ import 'dart:io'; -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_svg/flowy_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; class FlowyErrorPage extends StatelessWidget { factory FlowyErrorPage.error( @@ -90,9 +86,7 @@ class FlowyErrorPage extends StatelessWidget { Listener( behavior: HitTestBehavior.translucent, onPointerDown: (_) async { - await getIt().setData( - ClipboardServiceData(plainText: message), - ); + await Clipboard.setData(ClipboardData(text: message)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -194,8 +188,8 @@ class StackTracePreview extends StatelessWidget { "Copy", ), useIntrinsicWidth: true, - onTap: () => getIt().setData( - ClipboardServiceData(plainText: stackTrace), + onTap: () => Clipboard.setData( + ClipboardData(text: stackTrace), ), ), ), @@ -258,14 +252,18 @@ class GitHubRedirectButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyButton( leftIconSize: const Size.square(_height), - text: FlowyText(LocaleKeys.appName.tr()), + text: const FlowyText( + "AppFlowy", + ), useIntrinsicWidth: true, leftIcon: const Padding( padding: EdgeInsets.all(4.0), child: FlowySvg(FlowySvgData('login/github-mark')), ), onTap: () async { - await afLaunchUri(_gitHubNewBugUri); + if (await canLaunchUrl(_gitHubNewBugUri)) { + await launchUrl(_gitHubNewBugUri); + } }, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index 5b0b791c6c..a34a55b9f8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -10,7 +10,6 @@ class FlowyTooltip extends StatelessWidget { this.preferBelow, this.margin, this.verticalOffset, - this.padding, this.child, }); @@ -20,7 +19,6 @@ class FlowyTooltip extends StatelessWidget { final EdgeInsetsGeometry? margin; final Widget? child; final double? verticalOffset; - final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -31,11 +29,10 @@ class FlowyTooltip extends StatelessWidget { return Tooltip( margin: margin, verticalOffset: verticalOffset ?? 16.0, - padding: padding ?? - const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), decoration: BoxDecoration( color: context.tooltipBackgroundColor(), borderRadius: BorderRadius.circular(10.0), @@ -50,77 +47,10 @@ class FlowyTooltip extends StatelessWidget { } } -class ManualTooltip extends StatefulWidget { - const ManualTooltip({ - super.key, - this.message, - this.richMessage, - this.preferBelow, - this.margin, - this.verticalOffset, - this.padding, - this.showAutomaticlly = false, - this.child, - }); - - final String? message; - final InlineSpan? richMessage; - final bool? preferBelow; - final EdgeInsetsGeometry? margin; - final Widget? child; - final double? verticalOffset; - final EdgeInsets? padding; - final bool showAutomaticlly; - - @override - State createState() => _ManualTooltipState(); -} - -class _ManualTooltipState extends State { - final key = GlobalKey(); - - @override - void initState() { - if (widget.showAutomaticlly) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) key.currentState?.ensureTooltipVisible(); - }); - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Tooltip( - key: key, - margin: widget.margin, - verticalOffset: widget.verticalOffset ?? 16.0, - triggerMode: widget.showAutomaticlly ? TooltipTriggerMode.manual : null, - padding: widget.padding ?? - const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - decoration: BoxDecoration( - color: context.tooltipBackgroundColor(), - borderRadius: BorderRadius.circular(10.0), - ), - waitDuration: _tooltipWaitDuration, - message: widget.message, - textStyle: widget.message != null ? context.tooltipTextStyle() : null, - richMessage: widget.richMessage, - preferBelow: widget.preferBelow, - child: widget.child, - ); - } -} - extension FlowyToolTipExtension on BuildContext { double tooltipFontSize() => 14.0; - double tooltipHeight({double? fontSize}) => 20.0 / (fontSize ?? tooltipFontSize()); - Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light ? Colors.white : Colors.black; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index b5b5c22bc7..62cb26d4e0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -31,8 +31,6 @@ dependencies: flowy_svg: path: ../flowy_svg - analyzer: 6.11.0 - dev_dependencies: build_runner: ^2.4.9 provider: ^6.0.5 diff --git a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart index e87bb3fa01..cbf114156d 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart @@ -242,13 +242,7 @@ String varNameFor(File file, Options options) { return simplified; } -const sizeMap = { - r'$16x': 's', - r'$20x': 'm', - r'$24x': 'm', - r'$32x': 'lg', - r'$40x': 'xl' -}; +const sizeMap = {r'$16x': 's', r'$24x': 'm', r'$32x': 'lg', r'$40x': 'xl'}; /// cleans the path segment before rejoining the path into a variable name String clean(String segment) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1a393e1180..fc04deae5b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -15,7 +15,7 @@ packages: source: sdk version: "0.3.3" analyzer: - dependency: "direct main" + dependency: transitive description: name: analyzer sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" @@ -30,14 +30,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.11" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" - source: hosted - version: "2.0.3" any_date: dependency: "direct main" description: @@ -98,8 +90,8 @@ packages: dependency: "direct main" description: path: "." - ref: "680222f" - resolved-ref: "680222f503f90d07c08c99c42764f9b08fd0f46c" + ref: e2c9713 + resolved-ref: e2c9713104f00658c83ac7d1657e7a0fade171ad url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "5.1.0" @@ -107,8 +99,8 @@ packages: dependency: "direct main" description: path: "packages/appflowy_editor_plugins" - ref: "4efcff7" - resolved-ref: "4efcff720ed01dd4d0f5f88a9f1ff6f79f423caa" + ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb + resolved-ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" source: git version: "0.0.6" @@ -126,13 +118,6 @@ packages: relative: true source: path version: "0.0.1" - appflowy_ui: - dependency: "direct main" - description: - path: "packages/appflowy_ui" - relative: true - source: path - version: "1.0.0" archive: dependency: "direct main" description: @@ -268,18 +253,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "8.1.4" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -533,10 +518,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -798,10 +783,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "8.1.6" flutter_cache_manager: dependency: "direct main" description: @@ -1346,6 +1331,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.6" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" logging: dependency: transitive description: @@ -2193,30 +2186,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" - talker: - dependency: "direct main" - description: - name: talker - sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" - url: "https://pub.dev" - source: hosted - version: "4.7.1" - talker_bloc_logger: - dependency: "direct main" - description: - name: talker_bloc_logger - sha256: "2214a5f6ef9ff33494dc6149321c270356962725cc8fc1a485d44b1d9b812ddd" - url: "https://pub.dev" - source: hosted - version: "4.7.1" - talker_logger: - dependency: transitive - description: - name: talker_logger - sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 - url: "https://pub.dev" - source: hosted - version: "4.7.1" term_glyph: dependency: transitive description: @@ -2316,11 +2285,10 @@ packages: unsplash_client: dependency: "direct main" description: - path: "." - ref: a8411fc - resolved-ref: a8411fcead178834d1f4572f64dc78b059ffa221 - url: "https://github.com/LucasXu0/unsplash_client.git" - source: git + name: unsplash_client + sha256: "9827f4c1036b7a6ac8cb3f404ac179df7441eee69371d9b17f181817fe502fd7" + url: "https://pub.dev" + source: hosted version: "2.2.0" url_launcher: dependency: "direct main" @@ -2604,5 +2572,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.2 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.4" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 1e92765ff6..f70f88ede7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.8.9 +version: 0.8.6 environment: flutter: ">=3.27.4" @@ -25,8 +25,7 @@ dependencies: path: packages/appflowy_popover appflowy_result: path: packages/appflowy_result - appflowy_ui: - path: packages/appflowy_ui + archive: ^3.4.10 auto_size_text_field: ^2.2.3 auto_updater: ^1.0.0 @@ -34,7 +33,7 @@ dependencies: # BitsDojo Window for Windows bitsdojo_window: ^0.1.6 - bloc: ^9.0.0 + bloc: ^8.1.2 cached_network_image: ^3.3.0 calendar_view: git: @@ -68,7 +67,7 @@ dependencies: flutter: sdk: flutter flutter_animate: ^4.5.0 - flutter_bloc: ^9.1.0 + flutter_bloc: ^8.1.3 flutter_cache_manager: ^3.3.1 flutter_chat_core: 0.0.2 flutter_chat_ui: ^2.0.0-dev.1 @@ -149,15 +148,9 @@ dependencies: xml: ^6.5.0 window_manager: ^0.4.3 saver_gallery: ^4.0.1 - talker_bloc_logger: ^4.7.1 - talker: ^4.7.1 - - analyzer: 6.11.0 dev_dependencies: - # Introduce talker to log the bloc events, and only log the events in the development mode - - bloc_test: ^10.0.0 + bloc_test: ^9.1.2 build_runner: ^2.4.9 envied_generator: ^1.0.1 flutter_lints: ^5.0.0 @@ -177,7 +170,7 @@ dev_dependencies: dependency_overrides: http: ^1.0.0 - device_info_plus: ^11.2.2 + device_info_plus: ^10.1.0 url_protocol: git: @@ -187,13 +180,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "680222f" + ref: "e2c9713" appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git path: "packages/appflowy_editor_plugins" - ref: "4efcff7" + ref: "ca8289099e40e0d6ad0605fbbe01fde3091538bb" sheet: git: @@ -237,11 +230,6 @@ dependency_overrides: path: packages/auto_updater_platform_interface ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458 - unsplash_client: - git: - url: https://github.com/LucasXu0/unsplash_client.git - ref: a8411fc - # auto_updater: # path: /Users/lucas.xu/Desktop/auto_updater/packages/auto_updater @@ -256,6 +244,9 @@ flutter: uses-material-design: true fonts: + - family: FlowyIconData + fonts: + - asset: assets/fonts/FlowyIconData.ttf - family: Poppins fonts: - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf @@ -281,9 +272,6 @@ flutter: - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf style: italic - # White-label font configuration will be added here - # BEGIN: WHITE_LABEL_FONT - # END: WHITE_LABEL_FONT # To add assets to your application, add an assets section, like this: assets: @@ -292,7 +280,6 @@ flutter: - assets/images/built_in_cover_images/ - assets/flowy_icons/ - assets/flowy_icons/16x/ - - assets/flowy_icons/20x/ - assets/flowy_icons/24x/ - assets/flowy_icons/32x/ - assets/flowy_icons/40x/ @@ -300,7 +287,6 @@ flutter: - assets/images/login/ - assets/translations/ - assets/icons/icons.json - - assets/fonts/ # The following assets will be excluded in release. # BEGIN: EXCLUDE_IN_RELEASE diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart index 46b8118087..73ad2736a0 100644 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart @@ -23,15 +23,11 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -40,7 +36,7 @@ class _MockAIRepository extends Mock implements AppFlowyAIService { final lines = text.split('\n'); for (final line in lines) { if (line.isNotEmpty) { - await processMessage('$_aiResponse $line\n\n'); + await onProcess('$_aiResponse $line\n\n'); } } await onEnd(); @@ -57,22 +53,18 @@ class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( Future(() async { await onStart(); // only return 1 line. - await processMessage('Hello World'); + await onProcess('Hello World'); await onEnd(); }), ); @@ -87,15 +79,11 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -103,7 +91,7 @@ class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { await onStart(); // return 10 lines for (var i = 0; i < 10; i++) { - await processMessage('Hello World\n\n'); + await onProcess('Hello World\n\n'); } await onEnd(); }), @@ -119,15 +107,11 @@ class _MockErrorRepository extends Mock implements AppFlowyAIService { required String text, PredefinedFormat? format, List sourceIds = const [], - List history = const [], required CompletionTypePB completionType, required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, + required Future Function(String text) onProcess, required Future Function() onEnd, required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, }) async { final stream = _MockCompletionStream(); unawaited( @@ -174,22 +158,20 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; + final command = AiWriterCommand.explain; + final node = aiWriterNode( + command: command, + selection: selection, + ); return AiWriterCubit( documentId: '', + getAiWriterNode: () => node, editorState: editorState, + initialCommand: command, aiService: _MockAIRepository(), ); }, - act: (bloc) => bloc.register( - aiWriterNode( - command: AiWriterCommand.explain, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ), - ), - ), - wait: Duration(seconds: 1), + act: (bloc) => bloc.init(), expect: () => [ isA() .having((s) => s.markdownText, 'result', isEmpty), @@ -230,22 +212,19 @@ void main() { ); final editorState = EditorState(document: document) ..selection = selection; + final node = aiWriterNode( + command: AiWriterCommand.explain, + selection: selection, + ); return AiWriterCubit( documentId: '', + getAiWriterNode: () => node, editorState: editorState, + initialCommand: AiWriterCommand.explain, aiService: _MockErrorRepository(), ); }, - act: (bloc) => bloc.register( - aiWriterNode( - command: AiWriterCommand.explain, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ), - ), - ), - wait: Duration(seconds: 1), + act: (bloc) => bloc.init(), expect: () => [ isA() .having((s) => s.markdownText, 'result', isEmpty), @@ -281,10 +260,12 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', + getAiWriterNode: () => aiNode, editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepository(), ); - bloc.register(aiNode); + bloc.init(); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); @@ -329,10 +310,12 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', + getAiWriterNode: () => aiNode, editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepository(), ); - bloc.register(aiNode); + bloc.init(); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.discard); await blocResponseFuture(); @@ -368,14 +351,16 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', + getAiWriterNode: () => aiNode, editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepositoryLess(), ); - bloc.register(aiNode); + bloc.init(); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); - expect(editorState.document.root.children.length, 2); + expect(editorState.document.root.children.length, 1); expect( editorState.getNodeAtPath([0])!.delta!.toPlainText(), 'Hello World', @@ -405,10 +390,12 @@ void main() { final aiNode = editorState.getNodeAtPath([3])!; final bloc = AiWriterCubit( documentId: '', + getAiWriterNode: () => aiNode, editorState: editorState, + initialCommand: AiWriterCommand.improveWriting, aiService: _MockAIRepositoryMore(), ); - bloc.register(aiNode); + bloc.init(); await blocResponseFuture(); bloc.runResponseAction(SuggestionAction.accept); await blocResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index 41865b7dd7..d6d0351414 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -182,14 +182,14 @@ void main() { await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); - var workspaceLatest = + var workspaceSetting = await FolderEventGetCurrentWorkspaceSetting().send().then( (result) => result.fold( (l) => l, (r) => throw Exception(), ), ); - workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id; + workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id; // ignore: unused_local_variable final documentBloc = DocumentBloc(documentId: document.id) @@ -198,13 +198,14 @@ void main() { ); await blocResponseFuture(); - workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceLatest.latestView.id == document.id; + workspaceSetting = + await FolderEventGetCurrentWorkspaceSetting().send().then( + (result) => result.fold( + (l) => l, + (r) => throw Exception(), + ), + ); + workspaceSetting.latestView.id == document.id; }); test('create views', () async { diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart index 8b1b710f4e..0b6289c784 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart @@ -294,578 +294,6 @@ void main() { ); }); }); - - group('markdown text robot - replace in same line:', () { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - Document buildTestDocument() { - return Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1 + text2 + text3)), - ], - ), - ); - } - - // 1. create a document with a paragraph node - // 2. use the text robot to replace the selected content in the same line - // 3. check the document - test('the selection is in the middle of the text', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: text1.length, - ), - end: Position( - path: [0], - offset: text1.length + text2.length, - ), - ); - - final markdownText = - '''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 5); - - final d1 = afterDelta[0] as TextInsert; - expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the '); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'World Wide Web'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect(d3.text, ' transformed the internet, making it accessible to '); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'non-technical users'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect( - d5.text, - ' and opening the floodgates for global mass adoption.$text3', - ); - expect(d5.attributes, null); - }); - - test('replace markdown text with selection from start to middle', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - ), - end: Position( - path: [0], - offset: text1.length, - ), - ); - - final markdownText = - '''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 5); - - final d1 = afterDelta[0] as TextInsert; - expect(d1.text, 'The '); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'invention'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect(d3.text, ' of the '); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'World Wide Web'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect( - d5.text, - ' by Tim Berners-Lee transformed how we access information.$text2$text3', - ); - expect(d5.attributes, null); - }); - - test('replace markdown text with selection from middle to end', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: text1.length + text2.length, - ), - end: Position( - path: [0], - offset: text1.length + text2.length + text3.length, - ), - ); - - final markdownText = - '''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 7); - - final d1 = afterDelta[0] as TextInsert; - expect( - d1.text, - text1 + text2, - ); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'Email'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect( - d3.text, - ' became widespread, and instant messaging services like ', - ); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'ICQ'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect(d5.text, ' and '); - expect(d5.attributes, null); - - final d6 = afterDelta[5] as TextInsert; - expect( - d6.text, - 'AOL Instant Messenger', - ); - expect(d6.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d7 = afterDelta[6] as TextInsert; - expect( - d7.text, - ' gained tremendous popularity, allowing for seamless real-time text communication across the globe.', - ); - expect(d7.attributes, null); - }); - - test('replace markdown text with selection from start to end', () async { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point.'''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption.'''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - final document = Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1)), - paragraphNode(delta: Delta()..insert(text2)), - paragraphNode(delta: Delta()..insert(text3)), - ], - ), - ); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: text1.length), - ); - - final markdownText = '''1. $text1 - -2. $text1 - -3. $text1'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final nodes = editorState.document.root.children; - expect(nodes.length, 5); - - final d1 = nodes[0].delta!.toList()[0] as TextInsert; - expect(d1.text, text1); - expect(d1.attributes, null); - expect(nodes[0].type, NumberedListBlockKeys.type); - - final d2 = nodes[1].delta!.toList()[0] as TextInsert; - expect(d2.text, text1); - expect(d2.attributes, null); - expect(nodes[1].type, NumberedListBlockKeys.type); - - final d3 = nodes[2].delta!.toList()[0] as TextInsert; - expect(d3.text, text1); - expect(d3.attributes, null); - expect(nodes[2].type, NumberedListBlockKeys.type); - - final d4 = nodes[3].delta!.toList()[0] as TextInsert; - expect(d4.text, text2); - expect(d4.attributes, null); - - final d5 = nodes[4].delta!.toList()[0] as TextInsert; - expect(d5.text, text3); - expect(d5.attributes, null); - }); - }); - - group('markdown text robot - replace in multiple lines:', () { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - Document buildTestDocument() { - return Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1)), - paragraphNode(delta: Delta()..insert(text2)), - paragraphNode(delta: Delta()..insert(text3)), - ], - ), - ); - } - - // 1. create a document with 3 paragraph nodes - // 2. use the text robot to replace the selected content in the multiple lines - // 3. check the document - test( - 'the selection starts with the first paragraph and ends with the middle of second paragraph', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - ), - end: Position( - path: [1], - offset: text2.length - - ', opening the floodgates for mass adoption. '.length, - ), - ); - - final markdownText = - '''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point. - -Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 3); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, 'The '); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, 'introduction'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, ' of the World Wide Web in the '); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, 'early 1990s'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, ' marked a significant turning point.'); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 3); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, "Tim Berners-Lee's "); - expect(d1.attributes, null); - - final d2 = delta2[1] as TextInsert; - expect(d2.text, "revolutionary invention"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta2[2] as TextInsert; - expect( - d3.text, - " made the internet accessible to non-technical users, opening the floodgates for mass adoption. ", - ); - expect(d3.attributes, null); - } - - { - // third paragraph - final delta3 = afterNodes[2].delta!.toList(); - expect(delta3.length, 1); - - final d1 = delta3[0] as TextInsert; - expect(d1.text, text3); - expect(d1.attributes, null); - } - }); - - test( - 'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: 'The introduction of the World Wide Web'.length, - ), - end: Position( - path: [2], - offset: - 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' - .length, - ), - ); - - final markdownText = - ''' in the **early 1990s** marked a *significant turning point* in technological history. - -Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*. - -Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity - '''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 3); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, 'The introduction of the World Wide Web in the '); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, 'early 1990s'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, ' marked a '); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, 'significant turning point'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, ' in technological history.'); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 5); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, "Tim Berners-Lee's "); - expect(d1.attributes, null); - - final d2 = delta2[1] as TextInsert; - expect(d2.text, "revolutionary invention"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta2[2] as TextInsert; - expect( - d3.text, - " made the internet accessible to non-technical users, opening the floodgates for ", - ); - expect(d3.attributes, null); - - final d4 = delta2[3] as TextInsert; - expect(d4.text, "unprecedented mass adoption"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta2[4] as TextInsert; - expect(d5.text, "."); - expect(d5.attributes, null); - } - - { - // third paragraph - // third paragraph - final delta3 = afterNodes[2].delta!.toList(); - expect(delta3.length, 7); - - final d1 = delta3[0] as TextInsert; - expect(d1.text, "Email became "); - expect(d1.attributes, null); - - final d2 = delta3[1] as TextInsert; - expect(d2.text, "widely prevalent"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta3[2] as TextInsert; - expect(d3.text, ", and instant messaging services like "); - expect(d3.attributes, null); - - final d4 = delta3[3] as TextInsert; - expect(d4.text, "ICQ"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta3[4] as TextInsert; - expect(d5.text, " and "); - expect(d5.attributes, null); - - final d6 = delta3[5] as TextInsert; - expect(d6.text, "AOL Instant Messenger"); - expect(d6.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d7 = delta3[6] as TextInsert; - expect( - d7.text, - " gained tremendous popularity, allowing for real-time text communication.", - ); - expect(d7.attributes, null); - } - }); - - test( - 'the length of the returned response less than the length of the selected text', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: 'The introduction of the World Wide Web'.length, - ), - end: Position( - path: [2], - offset: - 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' - .length, - ), - ); - - final markdownText = - ''' in the **early 1990s** marked a *significant turning point* in technological history.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 2); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, "The introduction of the World Wide Web in the "); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, "early 1990s"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, " marked a "); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, "significant turning point"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, " in technological history."); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 1); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, ", allowing for real-time text communication."); - expect(d1.attributes, null); - } - }); - }); } const _sample1 = '''# The Curious Cat diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart index d2432557eb..387375c286 100644 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart @@ -1,8 +1,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide quoteNode, QuoteBlockKeys; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -24,6 +23,11 @@ void main() { void Function(EditorState editorState, Node node)? afterTurnInto, }) async { final editorState = EditorState(document: document); + final cubit = BlockActionOptionCubit( + editorState: editorState, + blockComponentBuilder: {}, + ); + final types = toType == null ? EditorOptionActionType.turnInto.supportTypes : [toType]; @@ -38,12 +42,7 @@ void main() { final node = editorState.getNodeAtPath([0])!; expect(node.type, originalType); - final result = await BlockActionOptionCubit.turnIntoBlock( - type, - node, - editorState, - level: level, - ); + final result = await cubit.turnIntoBlock(type, node, level: level); expect(result, true); final newNode = editorState.getNodeAtPath([0])!; expect(newNode.type, type); @@ -59,10 +58,9 @@ void main() { Selection.collapsed( Position(path: [0]), ); - await BlockActionOptionCubit.turnIntoBlock( + await cubit.turnIntoBlock( originalType, newNode, - editorState, ); expect(result, true); } @@ -165,6 +163,8 @@ void main() { for (final type in [ HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, ]) { test('from nested bulleted list to $type', () async { const text = 'bulleted list'; @@ -229,6 +229,8 @@ void main() { for (final type in [ HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, ]) { test('from nested numbered list to $type', () async { const text = 'numbered list'; @@ -293,6 +295,8 @@ void main() { for (final type in [ HeadingBlockKeys.type, + QuoteBlockKeys.type, + CalloutBlockKeys.type, ]) { // numbered list, bulleted list, todo list // before @@ -387,8 +391,6 @@ void main() { BulletedListBlockKeys.type, NumberedListBlockKeys.type, TodoListBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, ]) { // numbered list, bulleted list, todo list // before diff --git a/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart b/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart deleted file mode 100644 index 5b6f88801a..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() async { - test( - 'description', - () async { - final links = [ - 'https://www.baidu.com/', - 'https://appflowy.io/', - 'https://github.com/AppFlowy-IO/AppFlowy', - 'https://github.com/', - 'https://www.figma.com/design/3K0ai4FhDOJ3Lts8G3KOVP/Page?node-id=7282-4007&p=f&t=rpfvEvh9K9J9WkIo-0', - 'https://www.figma.com/files/drafts', - 'https://www.youtube.com/watch?v=LyY5Rh9qBvA', - 'https://www.youtube.com/', - 'https://www.youtube.com/watch?v=a6GDT7', - 'http://www.test.com/', - 'https://www.baidu.com/s?wd=test&rsv_spt=1&rsv_iqid=0xb6a7840b00e5324a&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=22073068_7_oem_dg&rsv_dl=tb&rsv_enter=1&rsv_sug3=5&rsv_sug1=4&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=test&rsp=9&inputT=478&rsv_sug4=547', - 'https://www.google.com/', - 'https://www.google.com.hk/search?q=test&oq=test&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIHCAQQABiABDIHCAUQABiABDIHCAYQABiABDIHCAcQABiABDIHCAgQLhiABDIHCAkQABiABNIBCTE4MDJqMGoxNagCCLACAfEFAQs7K9PprSfxBQELOyvT6a0n&sourceid=chrome&ie=UTF-8', - 'www.baidu.com', - 'baidu.com', - 'com', - 'https://www.baidu.com', - 'https://github.com/AppFlowy-IO/AppFlowy', - 'https://appflowy.com/app/c29fafc4-b7c0-4549-8702-71339b0fd9ea/59f36be8-9b2f-4d3e-b6a1-816c6c2043e5?blockId=GCY_T4', - ]; - - final parser = DefaultParser(); - int i = 1; - for (final link in links) { - final formatLink = LinkInfoParser.formatUrl(link); - final siteInfo = await parser - .parse(Uri.tryParse(formatLink) ?? Uri.parse(formatLink)); - if (siteInfo?.isEmpty() ?? true) { - debugPrint('$i : $formatLink ---- empty \n'); - } else { - debugPrint('$i : $formatLink ---- \n$siteInfo \n'); - } - i++; - } - }, - timeout: const Timeout(Duration(seconds: 120)), - ); -} diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 3bb774411b..c4f2a21c64 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -69,10 +69,7 @@ class AppFlowyUnitTest { } Future _initialServices() async { - workspaceService = WorkspaceService( - workspaceId: currentWorkspace.id, - userId: userProfile.id, - ); + workspaceService = WorkspaceService(workspaceId: currentWorkspace.id); } Future createWorkspace() async { diff --git a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart b/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart index 8a370f74d5..4458d588cc 100644 --- a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -30,17 +29,14 @@ void main() { showDialog( context: context, builder: (_) { - return AppFlowyTheme( - data: AppFlowyDefaultTheme().light(), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ConfirmPopup( - description: "desc", - title: "title", - onConfirm: onConfirm, - ), + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: ConfirmPopup( + description: "desc", + title: "title", + onConfirm: onConfirm, ), ); }, diff --git a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart b/frontend/appflowy_flutter/test/widget_test/test_material_app.dart index 2ebc61a8bc..ecb06b97e2 100644 --- a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart +++ b/frontend/appflowy_flutter/test/widget_test/test_material_app.dart @@ -62,7 +62,6 @@ class WidgetTestApp extends StatelessWidget { scrollbarColor: Colors.transparent, scrollbarHoverColor: Colors.transparent, lightIconColor: Colors.transparent, - toolbarHoverColor: Colors.transparent, ), ], ), diff --git a/frontend/resources/flowy_icons/16x/ai_explain.svg b/frontend/resources/flowy_icons/16x/ai_explain.svg new file mode 100644 index 0000000000..173b35b543 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_explain.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/16x/ai_chat_logo.svg b/frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/ai_chat_logo.svg rename to frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg diff --git a/frontend/resources/flowy_icons/16x/app_logo.svg b/frontend/resources/flowy_icons/16x/flowy_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/app_logo.svg rename to frontend/resources/flowy_icons/16x/flowy_logo.svg diff --git a/frontend/resources/flowy_icons/16x/help_and_documentation.svg b/frontend/resources/flowy_icons/16x/help_and_documentation.svg deleted file mode 100644 index e4c68c2583..0000000000 --- a/frontend/resources/flowy_icons/16x/help_and_documentation.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/ai_explain.svg b/frontend/resources/flowy_icons/20x/ai_explain.svg deleted file mode 100644 index f490472688..0000000000 --- a/frontend/resources/flowy_icons/20x/ai_explain.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/anonymous_mode.svg b/frontend/resources/flowy_icons/20x/anonymous_mode.svg deleted file mode 100644 index bee519e54a..0000000000 --- a/frontend/resources/flowy_icons/20x/anonymous_mode.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/cloud_mode.svg b/frontend/resources/flowy_icons/20x/cloud_mode.svg deleted file mode 100644 index 5aaf68e3db..0000000000 --- a/frontend/resources/flowy_icons/20x/cloud_mode.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/embed_fullscreen.svg b/frontend/resources/flowy_icons/20x/embed_fullscreen.svg deleted file mode 100644 index b8b197fb13..0000000000 --- a/frontend/resources/flowy_icons/20x/embed_fullscreen.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/hide_password.svg b/frontend/resources/flowy_icons/20x/hide_password.svg deleted file mode 100644 index 2ebd274866..0000000000 --- a/frontend/resources/flowy_icons/20x/hide_password.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/resources/flowy_icons/20x/password_close.svg b/frontend/resources/flowy_icons/20x/password_close.svg deleted file mode 100644 index 52a44e1a8e..0000000000 --- a/frontend/resources/flowy_icons/20x/password_close.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_ai.svg b/frontend/resources/flowy_icons/20x/settings_page_ai.svg deleted file mode 100644 index d98a0c90fd..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_ai.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_bell.svg b/frontend/resources/flowy_icons/20x/settings_page_bell.svg deleted file mode 100644 index 57031d1f90..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_bell.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_cloud.svg b/frontend/resources/flowy_icons/20x/settings_page_cloud.svg deleted file mode 100644 index 44c20bb51b..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_cloud.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg b/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg deleted file mode 100644 index e1c64ee509..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_database.svg b/frontend/resources/flowy_icons/20x/settings_page_database.svg deleted file mode 100644 index bfbae5f8fe..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_database.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_earth.svg b/frontend/resources/flowy_icons/20x/settings_page_earth.svg deleted file mode 100644 index 0a205592b4..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_earth.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg b/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg deleted file mode 100644 index 92efc30142..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_plan.svg b/frontend/resources/flowy_icons/20x/settings_page_plan.svg deleted file mode 100644 index 9792bd41c4..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_plan.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_user.svg b/frontend/resources/flowy_icons/20x/settings_page_user.svg deleted file mode 100644 index 94968ff06b..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_user.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_users.svg b/frontend/resources/flowy_icons/20x/settings_page_users.svg deleted file mode 100644 index eb65bf7192..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_users.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/settings_page_workspace.svg b/frontend/resources/flowy_icons/20x/settings_page_workspace.svg deleted file mode 100644 index e9a6eb9a10..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_workspace.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/show_password.svg b/frontend/resources/flowy_icons/20x/show_password.svg deleted file mode 100644 index ac8d092b37..0000000000 --- a/frontend/resources/flowy_icons/20x/show_password.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/sign_in_settings.svg b/frontend/resources/flowy_icons/20x/sign_in_settings.svg deleted file mode 100644 index 5d88d23086..0000000000 --- a/frontend/resources/flowy_icons/20x/sign_in_settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/slash_menu_image.svg b/frontend/resources/flowy_icons/20x/slash_menu_image.svg deleted file mode 100644 index f5b7917ad3..0000000000 --- a/frontend/resources/flowy_icons/20x/slash_menu_image.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg deleted file mode 100644 index dd0390d2d5..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg deleted file mode 100644 index a8c8657135..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_alignment.svg b/frontend/resources/flowy_icons/20x/toolbar_alignment.svg deleted file mode 100644 index 638ff3ece8..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_alignment.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg deleted file mode 100644 index e6ef664403..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg deleted file mode 100644 index 2e39539ab0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_bold.svg b/frontend/resources/flowy_icons/20x/toolbar_bold.svg deleted file mode 100644 index a131c6aa3e..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_bold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_check.svg b/frontend/resources/flowy_icons/20x/toolbar_check.svg deleted file mode 100644 index e59186292c..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg deleted file mode 100644 index c263b0c66b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg deleted file mode 100644 index bc17a7b05b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_link.svg b/frontend/resources/flowy_icons/20x/toolbar_link.svg deleted file mode 100644 index 8564d243d0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg b/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg deleted file mode 100644 index 57cb67da9a..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg b/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg deleted file mode 100644 index fc8765fa5b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg b/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg deleted file mode 100644 index e1061b914a..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_more.svg b/frontend/resources/flowy_icons/20x/toolbar_more.svg deleted file mode 100644 index d156f313a1..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_more.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg deleted file mode 100644 index 87c67115fb..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg deleted file mode 100644 index bcaebfe5d0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg deleted file mode 100644 index 68069290ce..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg deleted file mode 100644 index e96b00ac35..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_format.svg b/frontend/resources/flowy_icons/20x/toolbar_text_format.svg deleted file mode 100644 index 0f3cd07a01..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_format.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg deleted file mode 100644 index ab53a64b38..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/toolbar_underline.svg b/frontend/resources/flowy_icons/20x/toolbar_underline.svg deleted file mode 100644 index ea467a45d6..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_underline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/turninto.svg b/frontend/resources/flowy_icons/20x/turninto.svg deleted file mode 100644 index 598b870ec7..0000000000 --- a/frontend/resources/flowy_icons/20x/turninto.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_bulleted_list.svg b/frontend/resources/flowy_icons/20x/type_bulleted_list.svg deleted file mode 100644 index bc726f59ec..0000000000 --- a/frontend/resources/flowy_icons/20x/type_bulleted_list.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/resources/flowy_icons/20x/type_callout.svg b/frontend/resources/flowy_icons/20x/type_callout.svg deleted file mode 100644 index a933b4bbb3..0000000000 --- a/frontend/resources/flowy_icons/20x/type_callout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_font.svg b/frontend/resources/flowy_icons/20x/type_font.svg deleted file mode 100644 index d0b33b0277..0000000000 --- a/frontend/resources/flowy_icons/20x/type_font.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_formula.svg b/frontend/resources/flowy_icons/20x/type_formula.svg deleted file mode 100644 index 316c225b79..0000000000 --- a/frontend/resources/flowy_icons/20x/type_formula.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_h1.svg b/frontend/resources/flowy_icons/20x/type_h1.svg deleted file mode 100644 index a6a7f561cf..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_h2.svg b/frontend/resources/flowy_icons/20x/type_h2.svg deleted file mode 100644 index 9bba1b7d33..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_h3.svg b/frontend/resources/flowy_icons/20x/type_h3.svg deleted file mode 100644 index 3b231df67d..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h3.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_numbered_list.svg b/frontend/resources/flowy_icons/20x/type_numbered_list.svg deleted file mode 100644 index 23046f9b34..0000000000 --- a/frontend/resources/flowy_icons/20x/type_numbered_list.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/resources/flowy_icons/20x/type_page.svg b/frontend/resources/flowy_icons/20x/type_page.svg deleted file mode 100644 index 405548fcf7..0000000000 --- a/frontend/resources/flowy_icons/20x/type_page.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_quote.svg b/frontend/resources/flowy_icons/20x/type_quote.svg deleted file mode 100644 index 3564d92ff8..0000000000 --- a/frontend/resources/flowy_icons/20x/type_quote.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_strikethrough.svg b/frontend/resources/flowy_icons/20x/type_strikethrough.svg deleted file mode 100644 index dbf4e86116..0000000000 --- a/frontend/resources/flowy_icons/20x/type_strikethrough.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_text.svg b/frontend/resources/flowy_icons/20x/type_text.svg deleted file mode 100644 index 40335aa89b..0000000000 --- a/frontend/resources/flowy_icons/20x/type_text.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_todo.svg b/frontend/resources/flowy_icons/20x/type_todo.svg deleted file mode 100644 index 3d4f38ae9f..0000000000 --- a/frontend/resources/flowy_icons/20x/type_todo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h1.svg b/frontend/resources/flowy_icons/20x/type_toggle_h1.svg deleted file mode 100644 index 45cc7d3859..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h2.svg b/frontend/resources/flowy_icons/20x/type_toggle_h2.svg deleted file mode 100644 index 3dce8523e8..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h3.svg b/frontend/resources/flowy_icons/20x/type_toggle_h3.svg deleted file mode 100644 index e619a5f250..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h3.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/20x/type_toggle_list.svg b/frontend/resources/flowy_icons/20x/type_toggle_list.svg deleted file mode 100644 index 2cb1e83599..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_list.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/resources/flowy_icons/16x/calendar_layout.svg b/frontend/resources/flowy_icons/24x/calendar_layout.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/calendar_layout.svg rename to frontend/resources/flowy_icons/24x/calendar_layout.svg diff --git a/frontend/resources/flowy_icons/16x/close_filled.svg b/frontend/resources/flowy_icons/24x/close_filled.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/close_filled.svg rename to frontend/resources/flowy_icons/24x/close_filled.svg diff --git a/frontend/resources/flowy_icons/16x/database_layout.svg b/frontend/resources/flowy_icons/24x/database_layout.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/database_layout.svg rename to frontend/resources/flowy_icons/24x/database_layout.svg diff --git a/frontend/resources/flowy_icons/40x/embed_error.svg b/frontend/resources/flowy_icons/40x/embed_error.svg deleted file mode 100644 index 68196c7b7e..0000000000 --- a/frontend/resources/flowy_icons/40x/embed_error.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/frontend/resources/flowy_icons/40x/app_logo.svg b/frontend/resources/flowy_icons/40x/flowy_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo.svg rename to frontend/resources/flowy_icons/40x/flowy_logo.svg diff --git a/frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg b/frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg rename to frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg diff --git a/frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg b/frontend/resources/flowy_icons/40x/flowy_logo_text.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg rename to frontend/resources/flowy_icons/40x/flowy_logo_text.svg diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index e8ca8c4ceb..23b9d4abdc 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -155,8 +155,7 @@ "charCountLabel": "عدد الأحرف: ", "createdAtLabel": "تم إنشاؤه: ", "syncedAtLabel": "تم المزامنة: ", - "saveAsNewPage": "حفظ الرسائل في الصفحة", - "saveAsNewPageDisabled": "لا توجد رسائل متاحة" + "saveAsNewPage": "حفظ الرسائل في الصفحة" }, "importPanel": { "textAndMarkdown": "نص و Markdown", @@ -229,7 +228,6 @@ "indexingFile": "الفهرسة {}", "generatingResponse": "توليد الاستجابة", "selectSources": "اختر المصادر", - "currentPage": "الصفحة الحالية", "sourcesLimitReached": "يمكنك فقط تحديد ما يصل إلى 3 مستندات من المستوى العلوي ومستنداتها الفرعية", "sourceUnsupported": "نحن لا ندعم الدردشة مع قواعد البيانات في الوقت الحالي", "regenerate": "حاول ثانية", @@ -256,19 +254,12 @@ "bulletWithImageDescription": "@:chat.changeFormat.bullet مع الصورة", "tableWithImageDescription": "@:chat.changeFormat.table مع الصورة" }, - "switchModel": { - "label": "تبديل النموذج", - "localModel": "النموذج المحلي", - "cloudModel": "نموذج السحابة", - "autoModel": "آلي" - }, "selectBanner": { "saveButton": "أضف إلى...", "selectMessages": "حدد الرسائل", "nSelected": "{} تم التحديد", "allSelected": "جميعها محددة" - }, - "stopTooltip": "توقف عن التوليد" + } }, "trash": { "text": "المهملات", @@ -311,16 +302,14 @@ "questionBubble": { "shortcuts": "الاختصارات", "whatsNew": "ما هو الجديد؟", - "helpAndDocumentation": "المساعدة والتوثيق", - "getSupport": "احصل على الدعم", + "help": "المساعدة والدعم", "markdown": "Markdown", "debug": { "name": "معلومات التصحيح", "success": "تم نسخ معلومات التصحيح إلى الحافظة!", "fail": "تعذر نسخ معلومات التصحيح إلى الحافظة" }, - "feedback": "تعليق", - "help": "المساعدة والدعم" + "feedback": "تعليق" }, "menuAppHeader": { "moreButtonToolTip": "إزالة وإعادة تسمية والمزيد...", @@ -515,8 +504,6 @@ "settings": "إعدادات", "members": "الأعضاء", "trash": "سلة المحذوفات", - "helpAndDocumentation": "المساعدة والتوثيق", - "getSupport": "احصل على الدعم", "helpAndSupport": "المساعدة والدعم" }, "sites": { @@ -776,7 +763,6 @@ "alignLeft": "محاذاة النص إلى اليسار", "alignCenter": "محاذاة النص إلى الوسط", "alignRight": "محاذاة النص إلى اليمين", - "insertInlineMathEquation": "إدراج معادلة رياضية مضمنة", "undo": "التراجع", "redo": "الإعادة", "convertToParagraph": "تحويل الكتلة إلى فقرة", @@ -867,17 +853,11 @@ "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", - "localAINotReadyRetryLater": "جاري تهيئة الذكاء الاصطناعي المحلي، يرجى المحاولة مرة أخرى لاحقًا", - "localAIDisabled": "أنت تستخدم الذكاء الاصطناعي المحلي، ولكنه مُعطّل. يُرجى الانتقال إلى الإعدادات لتفعيله أو تجربة نموذج آخر.", - "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", - "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", "restartLocalAI": "إعادة تشغيل الذكاء الاصطناعي المحلي", "disableLocalAITitle": "تعطيل الذكاء الاصطناعي المحلي", "disableLocalAIDescription": "هل تريد تعطيل الذكاء الاصطناعي المحلي؟", "localAIToggleTitle": "التبديل لتفعيل أو تعطيل الذكاء الاصطناعي المحلي", - "localAIToggleSubTitle": "قم بتشغيل نماذج الذكاء الاصطناعي المحلية الأكثر تقدمًا داخل AppFlowy للحصول على أقصى درجات الخصوصية والأمان", "offlineAIInstruction1": "اتبع", "offlineAIInstruction2": "تعليمات", "offlineAIInstruction3": "لتفعيل الذكاء الاصطناعي دون اتصال بالإنترنت.", @@ -886,15 +866,7 @@ "offlineAIDownload3": "إنه أولا", "activeOfflineAI": "نشط", "downloadOfflineAI": "التنزيل", - "openModelDirectory": "افتح المجلد", - "laiNotReady": "لم يتم تثبيت تطبيق الذكاء الاصطناعي المحلي بشكل صحيح.", - "ollamaNotReady": "خادم Ollama غير جاهز.", - "pleaseFollowThese": "اتبع هؤلاء", - "instructions": "التعليمات", - "installOllamaLai": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", - "modelsMissing": "لم يتم العثور على النماذج المطلوبة.", - "downloadModel": "لتنزيلها.", - "startLocalAI": "قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي" + "openModelDirectory": "افتح المجلد" } }, "planPage": { @@ -1439,7 +1411,6 @@ "filterBy": "مصنف بواسطة...", "typeAValue": "اكتب قيمة ...", "layout": "تَخطِيط", - "compactMode": "الوضع المضغوط", "databaseLayout": "تَخطِيط", "viewList": { "zero": "0 مشاهدات", @@ -1676,7 +1647,8 @@ "url": { "launch": "فتح في المتصفح", "copy": "إنسخ الرابط", - "textFieldHint": "أدخل عنوان URL" + "textFieldHint": "أدخل عنوان URL", + "copiedNotification": "تمت نسخها إلى الحافظة!" }, "relation": { "relatedDatabasePlaceLabel": "قاعدة البيانات ذات الصلة", @@ -1786,10 +1758,7 @@ "aiWriter": "كاتب الذكاء الاصطناعي", "dateOrReminder": "التاريخ أو التذكير", "photoGallery": "معرض الصور", - "file": "الملف", - "twoColumns": "عمودين", - "threeColumns": "3 أعمدة", - "fourColumns": "4 أعمدة" + "file": "الملف" }, "subPage": { "name": "المستند", @@ -1812,16 +1781,6 @@ "referencedGrid": "الشبكة المشار إليها", "referencedCalendar": "التقويم المشار إليه", "referencedDocument": "الوثيقة المشار إليها", - "aiWriter": { - "userQuestion": "اسأل الذكاء الاصطناعي عن أي شيء", - "continueWriting": "استمر في الكتابة", - "fixSpelling": "تصحيح الأخطاء الإملائية والنحوية", - "improveWriting": "تحسين الكتابة", - "summarize": "تلخيص", - "explain": "اشرح", - "makeShorter": "اجعلها أقصر", - "makeLonger": "اجعلها أطول" - }, "autoGeneratorMenuItemName": "كاتب AI", "autoGeneratorTitleName": "AI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...", "autoGeneratorLearnMore": "يتعلم أكثر", @@ -2146,28 +2105,7 @@ "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": "اسم رابط الإدخال" + "resetToDefaultFont": "إعادة تعيين إلى الافتراضي" }, "errorBlock": { "theBlockIsNotSupported": "الإصدار الحالي لا يدعم هذا الحقل.", @@ -2623,7 +2561,6 @@ "accountLogin": "تسجيل الدخول إلى الحساب", "updateNameError": "فشل في تحديث الاسم", "updateIconError": "فشل في تحديث الأيقونة", - "aboutAppFlowy": "حول appName", "deleteAccount": { "title": "حذف الحساب", "subtitle": "احذف حسابك وجميع بياناتك بشكل دائم.", @@ -2632,12 +2569,12 @@ "dialogTitle": "حذف الحساب", "dialogContent1": "هل أنت متأكد أنك تريد حذف حسابك نهائياً؟", "dialogContent2": "لا يمكن التراجع عن هذا الإجراء، وسوف يؤدي إلى إزالة الوصول من جميع مساحات العمل، ومسح حسابك بالكامل، بما في ذلك مساحات العمل الخاصة، وإزالتك من جميع مساحات العمل المشتركة.", - "confirmHint1": "من فضلك اكتب \"@:newSettings.myAccount.deleteAccount.confirmHint3\" للتأكيد.", + "confirmHint1": "من فضلك اكتب \"حذف حسابي\" للتأكيد.", "confirmHint2": "أفهم أن هذا الإجراء لا رجعة فيه وسيؤدي إلى حذف حسابي وجميع البيانات المرتبطة به بشكل دائم.", "confirmHint3": "حذف حسابي", "checkToConfirmError": "يجب عليك تحديد المربع لتأكيد الحذف", "failedToGetCurrentUser": "فشل في الحصول على البريد الإلكتروني الحالي للمستخدم", - "confirmTextValidationFailed": "نص التأكيد الخاص بك لا يتطابق مع \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", + "confirmTextValidationFailed": "نص التأكيد الخاص بك لا يتطابق مع \"حذف حسابي\"", "deleteAccountSuccess": "تم حذف الحساب بنجاح" } }, @@ -2814,7 +2751,6 @@ "moreOptions": "المزيد من الخيارات", "collapse": "طي", "signInAgreement": "بالنقر فوق \"متابعة\" أعلاه، فإنك توافق على شروط استخدام AppFlowy", - "signInLocalAgreement": "من خلال النقر على \"البدء\" أعلاه، فإنك توافق على شروط وأحكام AppFlowy", "and": "و", "termOfUse": "شروط", "privacyPolicy": "سياسة الخصوصية", @@ -3194,50 +3130,6 @@ } }, "ai": { - "contentPolicyViolation": "فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى", - "textLimitReachedDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة للذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "imageLimitReachedDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يُرجى الترقية إلى الخطة الاحترافية أو شراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "limitReachedAction": { - "textDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. للحصول على المزيد من الاستجابات، يرجى", - "imageDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يرجى", - "upgrade": "ترقية", - "toThe": "الى", - "proPlan": "الخطة الاحترافية", - "orPurchaseAn": "أو شراء", - "aiAddon": "مَرافِق الذكاء الاصطناعي" - }, - "editing": "تحرير", - "analyzing": "تحليل", - "continueWritingEmptyDocumentTitle": "استمر في كتابة الخطأ", - "continueWritingEmptyDocumentDescription": "نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!", - "more": "أكثر" - }, - "autoUpdate": { - "criticalUpdateTitle": "التحديث ضروري للمتابعة", - "criticalUpdateDescription": "لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق.", - "criticalUpdateButton": "تحديث", - "bannerUpdateTitle": "النسخة الجديدة متاحة!", - "bannerUpdateDescription": "احصل على أحدث الميزات والإصلاحات. انقر على \"تحديث\" للتثبيت الآن.", - "bannerUpdateButton": "تحديث", - "settingsUpdateTitle": "الإصدار الجديد ({newVersion}) متاح!", - "settingsUpdateDescription": "الإصدار الحالي: {currentVersion} (الإصدار الرسمي) → {newVersion}", - "settingsUpdateButton": "تحديث", - "settingsUpdateWhatsNew": "ما الجديد" - }, - "lockPage": { - "lockPage": "مقفل", - "reLockPage": "إعادة القفل", - "lockTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود. انقر لفتح القفل.", - "pageLockedToast": "الصفحة مقفلة. التعديل معطل حتى يفتحها أحد.", - "lockedOperationTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود." - }, - "suggestion": { - "accept": "يقبل", - "keep": "يحفظ", - "discard": "تجاهل", - "close": "يغلق", - "tryAgain": "حاول ثانية", - "rewrite": "إعادة كتابة", - "insertBelow": "أدخل أدناه" + "contentPolicyViolation": "فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى" } } diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 8d98cb5cbc..f388cf9bd5 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -133,14 +133,14 @@ "questionBubble": { "shortcuts": "Dreceres", "whatsNew": "Què hi ha de nou?", + "help": "Ajuda i Suport", "markdown": "Reducció", "debug": { "name": "Informació de depuració", "success": "S'ha copiat la informació de depuració!", "fail": "No es pot copiar la informació de depuració" }, - "feedback": "Feedback", - "help": "Ajuda i Suport" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Suprimeix, canvia el nom i més...", diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index acfc571536..4acb7a1765 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -170,14 +170,14 @@ "questionBubble": { "shortcuts": "کورتە ڕێگاکان", "whatsNew": "نوێترین", + "help": "پشتیوانی و یارمەتی", "markdown": "Markdown", "debug": { "name": "زانیاری دیباگ", "success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!", "fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد" }, - "feedback": "فیدباک", - "help": "پشتیوانی و یارمەتی" + "feedback": "فیدباک" }, "menuAppHeader": { "moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...", diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 28750dd542..07e5a01bea 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -134,14 +134,14 @@ "questionBubble": { "shortcuts": "Klávesové zkratky", "whatsNew": "Co je nového?", + "help": "Pomoc a podpora", "markdown": "Markdown", "debug": { "name": "Debug informace", "success": "Debug informace zkopírovány do schránky!", "fail": "Nepodařilo se zkopáí" }, - "feedback": "Zpětná vazba", - "help": "Pomoc a podpora" + "feedback": "Zpětná vazba" }, "menuAppHeader": { "moreButtonToolTip": "Smazat, přejmenovat, a další...", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 65a7fbea05..996ed03b7d 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -252,14 +252,14 @@ "questionBubble": { "shortcuts": "Tastenkürzel", "whatsNew": "Was gibt es Neues?", + "help": "Hilfe & Support", "markdown": "Markdown", "debug": { "name": "Debug-Informationen", "success": "Debug-Informationen in die Zwischenablage kopiert!", "fail": "Debug-Informationen konnten nicht in die Zwischenablage kopiert werden" }, - "feedback": "Feedback", - "help": "Hilfe & Support" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Entfernen, umbenennen und mehr...", @@ -1625,7 +1625,8 @@ "url": { "launch": "Im Browser öffnen", "copy": "Webadresse kopieren", - "textFieldHint": "Gebe eine URL ein" + "textFieldHint": "Gebe eine URL ein", + "copiedNotification": "In die Zwischenablage kopiert!" }, "relation": { "relatedDatabasePlaceLabel": "Verwandte Datenbank", @@ -2516,11 +2517,11 @@ "dialogTitle": "Benutzerkonto löschen", "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst.", - "confirmHint1": "Geben Sie zur Bestätigung bitte „@:newSettings.myAccount.deleteAccount.confirmHint3“ ein.", + "confirmHint1": "Geben Sie zur Bestätigung bitte „MEIN KONTO LÖSCHEN“ ein.", "confirmHint3": "MEIN KONTO LÖSCHEN", "checkToConfirmError": "Sie müssen das Kontrollkästchen aktivieren, um das Löschen zu bestätigen", "failedToGetCurrentUser": "Aktuelle Benutzer-E-Mail konnte nicht abgerufen werden.", - "confirmTextValidationFailed": "Ihr Bestätigungstext stimmt nicht mit „@:newSettings.myAccount.deleteAccount.confirmHint3“ überein.", + "confirmTextValidationFailed": "Ihr Bestätigungstext stimmt nicht mit „MEIN KONTO LÖSCHEN“ überein.", "deleteAccountSuccess": "Konto erfolgreich gelöscht" } }, diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json index a329a8998c..34f43b87ac 100644 --- a/frontend/resources/translations/el-GR.json +++ b/frontend/resources/translations/el-GR.json @@ -724,7 +724,8 @@ "url": { "launch": "Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης", "copy": "Copy link to clipboard", - "textFieldHint": "Enter a URL" + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", @@ -1402,4 +1403,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 746833fd1f..3a78ef813f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -36,9 +36,7 @@ "loginButtonText": "Login", "loginStartWithAnonymous": "Continue with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", - "continueWithLocalModel": "Continue with local model", - "switchToAppFlowyCloud": "AppFlowy Cloud", - "anonymousMode": "Anonymous mode", + "anonymous": "Anonymous", "buttonText": "Sign In", "signingInText": "Signing in...", "forgotPassword": "Forgot Password?", @@ -49,7 +47,7 @@ "repeatPasswordEmptyError": "Repeat password can't be empty", "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", - "or": "or", + "or": "OR", "signInWithGoogle": "Continue with Google", "signInWithGithub": "Continue with GitHub", "signInWithDiscord": "Continue with Discord", @@ -70,22 +68,7 @@ "logIn": "Log in", "generalError": "Something went wrong. Please try again later", "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", - "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.", - "tokenHasExpiredOrInvalid": "The code has expired or is invalid. Please try again.", - "signingIn": "Signing in...", - "checkYourEmail": "Check your email", - "temporaryVerificationLinkSent": "A temporary verification link has been sent.\nPlease check your inbox at", - "temporaryVerificationCodeSent": "A temporary verification code has been sent.\nPlease check your inbox at", - "continueToSignIn": "Continue to sign in", - "backToLogin": "Back to login", - "enterCode": "Enter code", - "enterCodeManually": "Enter code manually", - "continueWithEmail": "Continue with email", - "enterPassword": "Enter password", - "loginAs": "Login as", - "invalidVerificationCode": "Please enter a valid verification code", - "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later.", - "invalidLoginCredentials": "Your password is incorrect, please try again" + "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes." }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -168,8 +151,7 @@ "charCountLabel": "Character count: ", "createdAtLabel": "Created: ", "syncedAtLabel": "Synced: ", - "saveAsNewPage": "Add messages to page", - "saveAsNewPageDisabled": "No messages available" + "saveAsNewPage": "Add messages to page" }, "importPanel": { "textAndMarkdown": "Text & Markdown", @@ -263,25 +245,18 @@ "number": "Numbered list", "table": "Table", "blankDescription": "Format response", - "defaultDescription": "Auto response format", + "defaultDescription": "Auto mode", "textWithImageDescription": "@:chat.changeFormat.text with image", "numberWithImageDescription": "@:chat.changeFormat.number with image", "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", "tableWithImageDescription": "@:chat.changeFormat.table with image" }, - "switchModel": { - "label": "Switch model", - "localModel": "Local Model", - "cloudModel": "Cloud Model", - "autoModel": "Auto" - }, "selectBanner": { "saveButton": "Add to …", "selectMessages": "Select messages", "nSelected": "{} selected", "allSelected": "All selected" - }, - "stopTooltip": "Stop generating" + } }, "trash": { "text": "Trash", @@ -324,8 +299,7 @@ "questionBubble": { "shortcuts": "Shortcuts", "whatsNew": "What's new?", - "helpAndDocumentation": "Help & documentation", - "getSupport": "Get support", + "help": "Help & Support", "markdown": "Markdown", "debug": { "name": "Debug Info", @@ -357,7 +331,8 @@ "header": "Header", "highlight": "Highlight", "color": "Color", - "addLink": "Add Link" + "addLink": "Add Link", + "link": "Link" }, "tooltip": { "lightMode": "Switch to Light mode", @@ -525,8 +500,7 @@ "settings": "Settings", "members": "Members", "trash": "Trash", - "helpAndDocumentation": "Help & documentation", - "getSupport": "Get Support" + "helpAndSupport": "Help & Support" }, "sites": { "title": "Sites", @@ -645,8 +619,7 @@ "theme": { "title": "Theme", "description": "Select a preset theme, or upload your own custom theme.", - "uploadCustomThemeTooltip": "Upload a custom theme", - "failedToLoadThemes": "Failed to load themes, please check your permission settings in System Settings > Privacy and Security > Files and Folders > @:appName" + "uploadCustomThemeTooltip": "Upload a custom theme" }, "workspaceFont": { "title": "Workspace font", @@ -863,7 +836,7 @@ "menuLabel": "AI Settings", "keys": { "enableAISearchTitle": "AI Search", - "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet, and models available in Ollama", + "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT 4-o, Claude 3,5, Llama 3.1, and Mistral 7B", "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", "llmModel": "Language Model", "llmModelType": "Language Model Type", @@ -874,20 +847,14 @@ "downloadAIModelButton": "Download", "downloadingModel": "Downloading", "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If it's slow, try toggling it off and on", + "localAIStart": "Local AI Chat is starting...", "localAILoading": "Local AI Chat Model is loading...", "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running", - "localAINotReadyRetryLater": "Local AI is initializing, please retry later", - "localAIDisabled": "You are using local AI, but it is disabled. Please go to settings to enable it or try different model", - "localAIInitializing": "Local AI is loading. This may take a few seconds depending on your device", - "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", - "failToLoadLocalAI": "Failed to start local AI.", - "restartLocalAI": "Restart", + "failToLoadLocalAI": "Failed to start local AI", + "restartLocalAI": "Restart Local AI", "disableLocalAITitle": "Disable local AI", "disableLocalAIDescription": "Do you want to disable local AI?", - "localAIToggleTitle": "AppFlowy Local AI (LAI)", - "localAIToggleSubTitle": "Run the most advanced local AI models within AppFlowy for ultimate privacy and security", + "localAIToggleTitle": "Toggle to enable or disable local AI", "offlineAIInstruction1": "Follow the", "offlineAIInstruction2": "instruction", "offlineAIInstruction3": "to enable offline AI.", @@ -896,14 +863,7 @@ "offlineAIDownload3": "it first", "activeOfflineAI": "Active", "downloadOfflineAI": "Download", - "openModelDirectory": "Open folder", - "laiNotReady": "The Local AI app was not installed correctly.", - "ollamaNotReady": "The Ollama server is not ready.", - "pleaseFollowThese": "Please follow these", - "instructions": "instructions", - "installOllamaLai": "to set up Ollama and AppFlowy Local AI.", - "modelsMissing": "Cannot find the required models: ", - "downloadModel": "to download them." + "openModelDirectory": "Open folder" } }, "planPage": { @@ -1307,10 +1267,10 @@ "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page", "enableRTLToolbarItems": "Enable RTL toolbar items", "members": { - "title": "Members", + "title": "Members settings", "inviteMembers": "Invite members", "inviteHint": "Invite by email", - "sendInvite": "Invite", + "sendInvite": "Send invite", "copyInviteLink": "Copy invite link", "label": "Members", "user": "User", @@ -1345,22 +1305,7 @@ "inviteMemberSuccess": "The invitation has been sent successfully", "failedToInviteMember": "Failed to invite member", "workspaceMembersError": "Oops, something went wrong", - "workspaceMembersErrorDescription": "We couldn't load the member list at this time. Please try again later", - "inviteLinkToAddMember": "Invite link to add member", - "clickToCopyLink": "Click to copy link", - "or": "or", - "generateANewLink": "generate a new link", - "inviteMemberByEmail": "Invite member by email", - "inviteMemberHintText": "Invite by email", - "resetInviteLink": "Reset the invite link", - "resetInviteLinkDescription": "Resetting will deactivate the current link for all space members and generate a new one. The previous link can only be managed through the", - "adminPanel": "Admin Panel", - "reset": "Reset", - "resetInviteLinkSuccess": "Invite link reset successfully", - "resetInviteLinkFailed": "Failed to reset the invite link", - "resetInviteLinkFailedDescription": "Please try again later", - "memberPageDescription1": "Access the", - "memberPageDescription2": "for guest and advanced user management." + "workspaceMembersErrorDescription": "We couldn't load the member list at this time. Please try again later" } }, "files": { @@ -1681,7 +1626,8 @@ "url": { "launch": "Open link in browser", "copy": "Copy link to clipboard", - "textFieldHint": "Enter a URL" + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", @@ -2041,28 +1987,7 @@ "failedDuplicateFindView": "Failed to duplicate page - original view not found" } }, - "cannotMoveToItsChildren": "Cannot move to its children", - "linkPreview": { - "typeSelection": { - "pasteAs": "Paste as", - "mention": "Mention", - "URL": "URL", - "bookmark": "Bookmark", - "embed": "Embed" - }, - "linkPreviewMenu": { - "toMetion": "Convert to Mention", - "toUrl": "Convert to URL", - "toEmbed": "Convert to Embed", - "toBookmark": "Convert to Bookmark", - "copyLink": "Copy Link", - "replace": "Replace", - "reload": "Reload", - "removeLink": "Remove Link", - "pasteHint": "Paste in https://...", - "unableToDisplay": "unable to display" - } - } + "cannotMoveToItsChildren": "Cannot move to its children" }, "outlineBlock": { "placeholder": "Table of Contents" @@ -2170,28 +2095,7 @@ "morePages": "more pages" }, "toolbar": { - "resetToDefaultFont": "Reset to default", - "textSize": "Text size", - "textColor": "Text color", - "h1": "Heading 1", - "h2": "Heading 2", - "h3": "Heading 3", - "alignLeft": "Align left", - "alignRight": "Align right", - "alignCenter": "Align center", - "link": "Link", - "textAlign": "Text align", - "moreOptions": "More options", - "font": "Font", - "inlineCode": "Inline code", - "suggestions": "Suggestions", - "turnInto": "Turn into", - "equation": "Equation", - "insert": "Insert", - "linkInputHint": "Paste link or search pages", - "pageOrURL": "Page or URL", - "linkName": "Link Name", - "linkNameHint": "Input link name" + "resetToDefaultFont": "Reset to default" }, "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", @@ -2326,7 +2230,7 @@ }, "message": { "copy": { - "success": "Copied to clipboard", + "success": "Copied!", "fail": "Unable to copy" } }, @@ -2493,9 +2397,9 @@ "link": "Link", "numberedList": "Numbered list", "numberedListShortForm": "Numbered", - "toggleHeading1ShortForm": "Toggle H1", - "toggleHeading2ShortForm": "Toggle H2", - "toggleHeading3ShortForm": "Toggle H3", + "toggleHeading1ShortForm": "Toggle h1", + "toggleHeading2ShortForm": "Toggle h2", + "toggleHeading3ShortForm": "Toggle h3", "quote": "Quote", "strikethrough": "Strikethrough", "text": "Text", @@ -2556,7 +2460,6 @@ "copyLink": "Copy link", "removeLink": "Remove link", "editLink": "Edit link", - "convertTo": "Convert to", "linkText": "Text", "linkTextHint": "Please enter text", "linkAddressHint": "Please enter URL", @@ -2636,7 +2539,7 @@ "noLogFiles": "There're no log files", "newSettings": { "myAccount": { - "title": "Account & App", + "title": "My account", "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", "profileLabel": "Account name & Profile image", "profileNamePlaceholder": "Enter your name", @@ -2655,41 +2558,14 @@ "dialogTitle": "Delete account", "dialogContent1": "Are you sure you want to permanently delete your account?", "dialogContent2": "This action cannot be undone, and will remove access from all workspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.", - "confirmHint1": "Please type \"@:newSettings.myAccount.deleteAccount.confirmHint3\" to confirm.", + "confirmHint1": "Please type \"DELETE MY ACCOUNT\" to confirm.", "confirmHint2": "I understand that this action is irreversible and will permanently delete my account and all associated data.", "confirmHint3": "DELETE MY ACCOUNT", "checkToConfirmError": "You must check the box to confirm deletion", "failedToGetCurrentUser": "Failed to get current user email", - "confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", + "confirmTextValidationFailed": "Your confirmation text does not match \"DELETE MY ACCOUNT\"", "deleteAccountSuccess": "Account deleted successfully" - }, - "password": { - "title": "Password", - "changePassword": "Change password", - "currentPassword": "Current password", - "newPassword": "New password", - "confirmNewPassword": "Confirm new password", - "setupPassword": "Setup password", - "error": { - "newPasswordIsRequired": "New password is required", - "confirmPasswordIsRequired": "Confirm password is required", - "passwordsDoNotMatch": "Passwords do not match", - "newPasswordIsSameAsCurrent": "New password is same as current password" - }, - "toast": { - "passwordUpdatedSuccessfully": "Password updated successfully", - "passwordUpdatedFailed": "Failed to update password", - "passwordSetupSuccessfully": "Password setup successfully", - "passwordSetupFailed": "Failed to setup password" - }, - "hint": { - "enterYourCurrentPassword": "Enter your current password", - "enterYourNewPassword": "Enter your new password", - "confirmYourNewPassword": "Confirm your new password" - } - }, - "myAccount": "My Account", - "myProfile": "My Profile" + } }, "workplace": { "name": "Workplace", @@ -2742,11 +2618,6 @@ "commandPalette": { "placeholder": "Search or ask a question...", "bestMatches": "Best matches", - "aiOverview": "AI overview", - "aiOverviewSource": "Reference sources", - "aiOverviewMoreDetails": "More details", - "pagePreview": "Content preview", - "clickToOpenPage": "Click to open page", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", @@ -2868,8 +2739,7 @@ "continueWithApple": "Continue with Apple ", "moreOptions": "More options", "collapse": "Collapse", - "signInAgreement": "By clicking \"Continue\" above, you agreed to \nAppFlowy's ", - "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to \nAppFlowy's ", + "signInAgreement": "By clicking \"Continue\" above, you agreed to AppFlowy's", "and": "and", "termOfUse": "Terms", "privacyPolicy": "Privacy Policy", @@ -3264,8 +3134,7 @@ "editing": "Editing", "analyzing": "Analyzing", "continueWritingEmptyDocumentTitle": "Continue writing error", - "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!", - "more": "More" + "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!" }, "autoUpdate": { "criticalUpdateTitle": "Update required to continue", @@ -3295,4 +3164,4 @@ "rewrite": "Rewrite", "insertBelow": "Insert below" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 5f947ea015..31174c3dc5 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -218,14 +218,14 @@ "questionBubble": { "shortcuts": "Atajos", "whatsNew": "¿Qué hay de nuevo?", + "help": "Ayuda y Soporte", "markdown": "Reducción", "debug": { "name": "Información de depuración", "success": "¡Información copiada!", "fail": "No fue posible copiar la información" }, - "feedback": "Comentario", - "help": "Ayuda y Soporte" + "feedback": "Comentario" }, "menuAppHeader": { "moreButtonToolTip": "Eliminar, renombrar y más...", @@ -866,7 +866,8 @@ "url": { "launch": "Abrir en el navegador", "copy": "Copiar URL", - "textFieldHint": "Introduce una URL" + "textFieldHint": "Introduce una URL", + "copiedNotification": "¡Copiado al portapapeles!" }, "relation": { "relatedDatabasePlaceLabel": "Base de datos relacionada", diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 2e52231f7c..be987e7a53 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -99,14 +99,14 @@ "questionBubble": { "shortcuts": "Lasterbideak", "whatsNew": "Ze berri?", + "help": "Laguntza", "markdown": "Markdown", "debug": { "name": "Debug informazioa", "success": "Debug informazioa kopiatu da!", "fail": "Ezin izan da debug informazioa kopiatu" }, - "feedback": "Iritzia", - "help": "Laguntza" + "feedback": "Iritzia" }, "menuAppHeader": { "addPageTooltip": "Gehitu orri bat", diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index cc93c17d64..80a100c3bc 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -139,14 +139,14 @@ "questionBubble": { "shortcuts": "میانبرها", "whatsNew": "تازه‌ترین‌ها", + "help": "پشتیبانی و مستندات", "markdown": "Markdown", "debug": { "name": "اطلاعات اشکال‌زدایی", "success": "طلاعات اشکال زدایی در کلیپ بورد کپی شد!", "fail": "نمی توان اطلاعات اشکال زدایی را در کلیپ بورد کپی کرد" }, - "feedback": "بازخورد", - "help": "پشتیبانی و مستندات" + "feedback": "بازخورد" }, "menuAppHeader": { "moreButtonToolTip": "حذف، تغییر نام، و موارد دیگر...", diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 589d2dfe18..7f8cdbd6a3 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -196,14 +196,14 @@ "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", + "help": "Aide et Support Technique", "markdown": "Réduction", "debug": { "name": "Infos du système", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour", - "help": "Aide et Support Technique" + "feedback": "Retour" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 989e21f349..270b92d72f 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -269,14 +269,14 @@ "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", + "help": "Aide et Support", "markdown": "Rédaction", "debug": { "name": "Informations de Débogage", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour", - "help": "Aide et Support" + "feedback": "Retour" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", @@ -1612,7 +1612,8 @@ "url": { "launch": "Ouvrir dans le navigateur", "copy": "Copier l'URL", - "textFieldHint": "Entrez une URL" + "textFieldHint": "Entrez une URL", + "copiedNotification": "Copié dans le presse-papier!" }, "relation": { "relatedDatabasePlaceLabel": "Base de données associée", @@ -2518,12 +2519,12 @@ "dialogTitle": "Supprimer le compte", "dialogContent1": "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?", "dialogContent2": "Cette action ne peut pas être annulée et supprimera l'accès à tous les espaces d'équipe, effaçant l'intégralité de votre compte, y compris les espaces de travail privés, et vous supprimant de tous les espaces de travail partagés.", - "confirmHint1": "Veuillez taper « @:newSettings.myAccount.deleteAccount.confirmHint3 » pour confirmer.", + "confirmHint1": "Veuillez taper « SUPPRIMER MON COMPTE » pour confirmer.", "confirmHint2": "Je comprends que cette action est irréversible et supprimera définitivement mon compte et toutes les données associées.", "confirmHint3": "SUPPRIMER MON COMPTE", "checkToConfirmError": "Vous devez cocher la case pour confirmer la suppression", "failedToGetCurrentUser": "Impossible d'obtenir l'e-mail de l'utilisateur actuel", - "confirmTextValidationFailed": "Votre texte de confirmation ne correspond pas à « @:newSettings.myAccount.deleteAccount.confirmHint3 »", + "confirmTextValidationFailed": "Votre texte de confirmation ne correspond pas à « SUPPRIMER MON COMPTE »", "deleteAccountSuccess": "Compte supprimé avec succès" } }, diff --git a/frontend/resources/translations/he.json b/frontend/resources/translations/he.json index 6c40f88947..d47c33c6da 100644 --- a/frontend/resources/translations/he.json +++ b/frontend/resources/translations/he.json @@ -206,14 +206,14 @@ "questionBubble": { "shortcuts": "מקשי קיצור", "whatsNew": "מה חדש?", + "help": "עזרה ותמיכה", "markdown": "Markdown", "debug": { "name": "פרטי ניפוי שגיאות", "success": "פרטי ניפוי השגיאות הועתקו ללוח הגזירים!", "fail": "לא ניתן להעתיק את פרטי ניפוי השגיאות ללוח הגזירים" }, - "feedback": "משוב", - "help": "עזרה ותמיכה" + "feedback": "משוב" }, "menuAppHeader": { "moreButtonToolTip": "הסרה, שינוי שם ועוד…", @@ -1243,7 +1243,8 @@ "url": { "launch": "פתיחת קישור בדפדפן", "copy": "העתקת קישור ללוח הגזירים", - "textFieldHint": "נא למלא כתובת" + "textFieldHint": "נא למלא כתובת", + "copiedNotification": "הועתק ללוח הגזירים!" }, "relation": { "relatedDatabasePlaceLabel": "מסד נתונים קשור", diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 1c10e40da4..40f05cccc6 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -103,14 +103,14 @@ "questionBubble": { "shortcuts": "Parancsikonok", "whatsNew": "Újdonságok", + "help": "Segítség & Támogatás", "markdown": "Markdown", "debug": { "name": "Debug Információ", "success": "Debug információ a vágólapra másolva", "fail": "A Debug információ nem másolható a vágólapra" }, - "feedback": "Visszacsatolás", - "help": "Segítség & Támogatás" + "feedback": "Visszacsatolás" }, "menuAppHeader": { "addPageTooltip": "Belső oldal hozzáadása", diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index b900929966..74d1e69d63 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -160,14 +160,14 @@ "questionBubble": { "shortcuts": "Pintasan", "whatsNew": "Apa yang baru?", + "help": "Bantuan & Dukungan", "markdown": "Penurunan harga", "debug": { "name": "Info debug", "success": "Info debug disalin ke papan klip!", "fail": "Tidak dapat menyalin info debug ke papan klip" }, - "feedback": "Masukan", - "help": "Bantuan & Dukungan" + "feedback": "Masukan" }, "menuAppHeader": { "moreButtonToolTip": "Menghapus, merubah nama, dan banyak lagi...", diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 7fb463da20..d6877ecd59 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -221,14 +221,14 @@ "questionBubble": { "shortcuts": "Scorciatoie", "whatsNew": "Cosa c'è di nuovo?", + "help": "Aiuto & Supporto", "markdown": "Markdown", "debug": { "name": "Informazioni di debug", "success": "Informazioni di debug copiate negli appunti!", "fail": "Impossibile copiare le informazioni di debug negli appunti" }, - "feedback": "Feedback", - "help": "Aiuto & Supporto" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Rimuovi, rinomina e altro...", diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index ebe679ad84..cd41f16530 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -263,14 +263,14 @@ "questionBubble": { "shortcuts": "ショートカット", "whatsNew": "新着情報", + "help": "ヘルプ & サポート", "markdown": "Markdown", "debug": { "name": "デバッグ情報", "success": "デバッグ情報をクリップボードにコピーしました!", "fail": "デバッグ情報をクリップボードにコピーできませんでした" }, - "feedback": "フィードバック", - "help": "ヘルプ & サポート" + "feedback": "フィードバック" }, "menuAppHeader": { "moreButtonToolTip": "削除、名前の変更、その他...", @@ -1577,7 +1577,8 @@ "url": { "launch": "リンクをブラウザで開く", "copy": "リンクをクリップボードにコピー", - "textFieldHint": "URLを入力" + "textFieldHint": "URLを入力", + "copiedNotification": "クリップボードにコピーされました!" }, "relation": { "relatedDatabasePlaceLabel": "関連データベース", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 1246b65f30..ef6c1cc67e 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -1,117 +1,62 @@ { "appName": "AppFlowy", - "defaultUsername": "나", - "welcomeText": "@:appName에 오신 것을 환영합니다", - "welcomeTo": "환영합니다", - "githubStarText": "GitHub에서 별표", + "defaultUsername": "Me", + "welcomeText": "@:appName 에 오신것을 환영합니다", + "githubStarText": "Star on GitHub", "subscribeNewsletterText": "뉴스레터 구독", - "letsGoButtonText": "빠른 시작", + "letsGoButtonText": "Let's Go", "title": "제목", - "youCanAlso": "또한 할 수 있습니다", + "youCanAlso": "당신은 또한 수", "and": "그리고", - "failedToOpenUrl": "URL을 열지 못했습니다: {}", "blockActions": { "addBelowTooltip": "아래에 추가하려면 클릭", "addAboveCmd": "Alt+클릭", "addAboveMacCmd": "Option+클릭", - "addAboveTooltip": "위에 추가하려면", - "dragTooltip": "이동하려면 드래그", - "openMenuTooltip": "메뉴를 열려면 클릭" + "addAboveTooltip": "위에 추가" }, "signUp": { - "buttonText": "가입하기", - "title": "@:appName에 가입하기", + "buttonText": "회원가입", + "title": "@:appName 에 회원가입", "getStartedText": "시작하기", - "emptyPasswordError": "비밀번호는 비워둘 수 없습니다", - "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", - "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", + "emptyPasswordError": "비밀번호는 공백일 수 없습니다", + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", "alreadyHaveAnAccount": "이미 계정이 있으신가요?", "emailHint": "이메일", "passwordHint": "비밀번호", - "repeatPasswordHint": "비밀번호 확인", - "signUpWith": "다음으로 가입:" + "repeatPasswordHint": "비밀번호 재입력" }, "signIn": { - "loginTitle": "@:appName에 로그인", + "loginTitle": "@:appName 에 로그인", "loginButtonText": "로그인", - "loginStartWithAnonymous": "익명 세션으로 계속", - "continueAnonymousUser": "익명 세션으로 계속", - "anonymous": "익명", "buttonText": "로그인", - "signingInText": "로그인 중...", "forgotPassword": "비밀번호를 잊으셨나요?", "emailHint": "이메일", "passwordHint": "비밀번호", "dontHaveAnAccount": "계정이 없으신가요?", - "createAccount": "계정 만들기", - "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", - "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", - "syncPromptMessage": "데이터 동기화에는 시간이 걸릴 수 있습니다. 이 페이지를 닫지 마세요", + "createAccount": "계정 생성", + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", "or": "또는", - "signInWithGoogle": "Google로 계속", - "signInWithGithub": "GitHub로 계속", - "signInWithDiscord": "Discord로 계속", - "signInWithApple": "Apple로 계속", - "continueAnotherWay": "다른 방법으로 계속", - "signUpWithGoogle": "Google로 가입", - "signUpWithGithub": "GitHub로 가입", - "signUpWithDiscord": "Discord로 가입", - "signInWith": "다음으로 계속:", - "signInWithEmail": "이메일로 계속", "signInWithMagicLink": "계속", - "signUpWithMagicLink": "Magic Link로 가입", "pleaseInputYourEmail": "이메일 주소를 입력하세요", "settings": "설정", - "magicLinkSent": "Magic Link가 전송되었습니다!", "invalidEmail": "유효한 이메일 주소를 입력하세요", - "alreadyHaveAnAccount": "이미 계정이 있으신가요?", + "alreadyHaveAnAccount": "이미 계정이 있나요?", "logIn": "로그인", - "generalError": "문제가 발생했습니다. 나중에 다시 시도하세요", - "limitRateError": "보안상의 이유로, 매 60초마다 한 번씩만 Magic Link를 요청할 수 있습니다", - "magicLinkSentDescription": "Magic Link가 이메일로 전송되었습니다. 링크를 클릭하여 로그인을 완료하세요. 링크는 5분 후에 만료됩니다." + "generalError": "오류가 발생했습니다. 나중에 다시 시도하세요", + "loginAsGuestButtonText": "시작하다" }, "workspace": { - "chooseWorkspace": "작업 공간 선택", - "defaultName": "내 작업 공간", - "create": "작업 공간 생성", - "new": "새 작업 공간", - "importFromNotion": "Notion에서 가져오기", - "learnMore": "자세히 알아보기", - "reset": "작업 공간 재설정", - "renameWorkspace": "작업 공간 이름 변경", - "workspaceNameCannotBeEmpty": "작업 공간 이름은 비워둘 수 없습니다", - "resetWorkspacePrompt": "작업 공간을 재설정하면 모든 페이지와 데이터가 삭제됩니다. 작업 공간을 재설정하시겠습니까? 또는 지원 팀에 문의하여 작업 공간을 복원할 수 있습니다", - "hint": "작업 공간", - "notFoundError": "작업 공간을 찾을 수 없습니다", - "failedToLoad": "문제가 발생했습니다! 작업 공간을 로드하지 못했습니다. @:appName의 모든 열린 인스턴스를 닫고 다시 시도하세요.", - "errorActions": { - "reportIssue": "문제 보고", - "reportIssueOnGithub": "GitHub에서 문제 보고", - "exportLogFiles": "로그 파일 내보내기", - "reachOut": "Discord에서 문의" - }, - "menuTitle": "작업 공간", - "deleteWorkspaceHintText": "작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다.", - "createSuccess": "작업 공간이 성공적으로 생성되었습니다", - "createFailed": "작업 공간 생성 실패", - "createLimitExceeded": "계정에 허용된 최대 작업 공간 수에 도달했습니다. 추가 작업 공간이 필요하면 GitHub에 요청하세요", - "deleteSuccess": "작업 공간이 성공적으로 삭제되었습니다", - "deleteFailed": "작업 공간 삭제 실패", - "openSuccess": "작업 공간이 성공적으로 열렸습니다", - "openFailed": "작업 공간 열기 실패", - "renameSuccess": "작업 공간 이름이 성공적으로 변경되었습니다", - "renameFailed": "작업 공간 이름 변경 실패", - "updateIconSuccess": "작업 공간 아이콘이 성공적으로 업데이트되었습니다", - "updateIconFailed": "작업 공간 아이콘 업데이트 실패", - "cannotDeleteTheOnlyWorkspace": "유일한 작업 공간을 삭제할 수 없습니다", - "fetchWorkspacesFailed": "작업 공간을 가져오지 못했습니다", - "leaveCurrentWorkspace": "작업 공간 나가기", - "leaveCurrentWorkspacePrompt": "현재 작업 공간을 나가시겠습니까?" + "defaultName": "내 워크스페이스", + "create": "워크스페이스 생성", + "hint": "워크스페이스", + "notFoundError": "워크스페이스를 찾을 수 없습니다" }, "shareAction": { "buttonText": "공유", - "workInProgress": "곧 출시 예정", - "markdown": "Markdown", + "workInProgress": "Coming soon", + "markdown": "마크다운", "html": "HTML", "clipboard": "클립보드에 복사", "csv": "CSV", @@ -121,1405 +66,352 @@ "publish": "게시", "unPublish": "게시 취소", "visitSite": "사이트 방문", - "exportAsTab": "다음으로 내보내기", + "exportAsTab": "내보내기", "publishTab": "게시", "shareTab": "공유", "publishOnAppFlowy": "AppFlowy에 게시", "shareTabTitle": "협업 초대", "shareTabDescription": "누구와도 쉽게 협업할 수 있습니다", - "copyLinkSuccess": "링크가 클립보드에 복사되었습니다", + "copyLinkSuccess": "클립보드에 링크 복사", "copyShareLink": "공유 링크 복사", - "copyLinkFailed": "링크를 클립보드에 복사하지 못했습니다", - "copyLinkToBlockSuccess": "블록 링크가 클립보드에 복사되었습니다", - "copyLinkToBlockFailed": "블록 링크를 클립보드에 복사하지 못했습니다", + "copyLinkFailed": "클립보드에 링크를 복사하는 데 실패했습니다", + "copyLinkToBlockSuccess": "블록 링크를 클립보드에 복사했습니다", + "copyLinkToBlockFailed": "블록 링크를 클립보드에 복사하는 데 실패했습니다", "manageAllSites": "모든 사이트 관리", "updatePathName": "경로 이름 업데이트" }, "moreAction": { - "small": "작게", + "small": "작은", "medium": "중간", - "large": "크게", + "large": "크기가 큰", "fontSize": "글꼴 크기", "import": "가져오기", - "moreOptions": "더 많은 옵션", - "wordCount": "단어 수: {}", - "charCount": "문자 수: {}", - "createdAt": "생성일: {}", - "deleteView": "삭제", - "duplicateView": "복제", - "wordCountLabel": "단어 수: ", - "charCountLabel": "문자 수: ", - "createdAtLabel": "생성일: ", - "syncedAtLabel": "동기화됨: ", - "saveAsNewPage": "페이지에 메시지 추가", - "saveAsNewPageDisabled": "사용 가능한 메시지가 없습니다" + "moreOptions": "추가 옵션" }, "importPanel": { - "textAndMarkdown": "텍스트 & Markdown", - "documentFromV010": "v0.1.0에서 문서 가져오기", - "databaseFromV010": "v0.1.0에서 데이터베이스 가져오기", - "notionZip": "Notion 내보낸 Zip 파일", + "textAndMarkdown": "텍스트 및 마크다운", + "documentFromV010": "v0.1.0의 문서", + "databaseFromV010": "v0.1.0의 데이터베이스", "csv": "CSV", - "database": "데이터베이스" - }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "파일을 드래그 앤 드롭하거나 클릭하여 ", - "placeholderUpload": "업로드", - "placeholderRight": "하거나 이미지 링크를 붙여넣으세요.", - "dropToUpload": "업로드할 파일을 드롭하세요", - "change": "변경" - } + "database": "데이터 베이스" }, "disclosureAction": { - "rename": "이름 변경", + "rename": "이름변경", "delete": "삭제", "duplicate": "복제", - "unfavorite": "즐겨찾기에서 제거", - "favorite": "즐겨찾기에 추가", - "openNewTab": "새 탭에서 열기", - "moveTo": "이동", - "addToFavorites": "즐겨찾기에 추가", - "copyLink": "링크 복사", - "changeIcon": "아이콘 변경", - "collapseAllPages": "모든 하위 페이지 접기", - "movePageTo": "페이지 이동", - "move": "이동", - "lockPage": "페이지 잠금" + "openNewTab": "새 탭에서 열기" }, "blankPageTitle": "빈 페이지", - "newPageText": "새 페이지", - "newDocumentText": "새 문서", - "newGridText": "새 그리드", - "newCalendarText": "새 캘린더", - "newBoardText": "새 보드", + "newPageText": "새로운 페이지", "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": "PDF, 텍스트 또는 마크다운 파일 첨부", - "questionDetail": "안녕하세요 {}! 오늘 어떻게 도와드릴까요?", - "indexingFile": "{} 색인화 중", - "generatingResponse": "응답 생성 중", - "selectSources": "출처 선택", - "currentPage": "현재 페이지", - "sourcesLimitReached": "최대 3개의 최상위 문서와 그 하위 문서만 선택할 수 있습니다", - "sourceUnsupported": "현재 데이터베이스와의 채팅을 지원하지 않습니다", - "regenerate": "다시 시도", - "addToPageButton": "페이지에 메시지 추가", - "addToPageTitle": "메시지 추가...", - "addToNewPage": "새 페이지 만들기", - "addToNewPageName": "\"{}\"에서 추출한 메시지", - "addToNewPageSuccessToast": "메시지가 추가되었습니다", - "openPagePreviewFailedToast": "페이지를 열지 못했습니다", - "changeFormat": { - "actionButton": "형식 변경", - "confirmButton": "이 형식으로 다시 생성", - "textOnly": "텍스트", - "imageOnly": "이미지 전용", - "textAndImage": "텍스트와 이미지", - "text": "단락", - "bullet": "글머리 기호 목록", - "number": "번호 매기기 목록", - "table": "표", - "blankDescription": "응답 형식", - "defaultDescription": "자동 모드", - "textWithImageDescription": "@:chat.changeFormat.text와 이미지", - "numberWithImageDescription": "@:chat.changeFormat.number와 이미지", - "bulletWithImageDescription": "@:chat.changeFormat.bullet와 이미지", - "tableWithImageDescription": "@:chat.changeFormat.table와 이미지" - }, - "selectBanner": { - "saveButton": "추가 ...", - "selectMessages": "메시지 선택", - "nSelected": "{}개 선택됨", - "allSelected": "모두 선택됨" - }, - "stopTooltip": "생성 중지" + "newChat": "AI 대화" }, "trash": { "text": "휴지통", - "restoreAll": "모두 복원", - "restore": "복원", + "restoreAll": "모두 복구", "deleteAll": "모두 삭제", "pageHeader": { "fileName": "파일 이름", - "lastModified": "마지막 수정", - "created": "생성됨" + "lastModified": "수정날짜", + "created": "생성날짜" }, "confirmDeleteAll": { - "title": "휴지통의 모든 페이지", - "caption": "휴지통의 모든 항목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "title": "휴지통의 모든 페이지를 삭제하시겠습니까?", + "caption": "이 작업은 취소할 수 없습니다." }, "confirmRestoreAll": { - "title": "휴지통의 모든 페이지 복원", - "caption": "이 작업은 되돌릴 수 없습니다." - }, - "restorePage": { - "title": "복원: {}", - "caption": "이 페이지를 복원하시겠습니까?" - }, - "mobile": { - "actions": "휴지통 작업", - "empty": "휴지통에 페이지나 공간이 없습니다", - "emptyDescription": "필요 없는 항목을 휴지통으로 이동하세요.", - "isDeleted": "삭제됨", - "isRestored": "복원됨" - }, - "confirmDeleteTitle": "이 페이지를 영구적으로 삭제하시겠습니까?" + "title": "휴지통의 모든 페이지를 복원하시겠습니까?", + "caption": "이 작업은 취소할 수 없습니다." + } }, "deletePagePrompt": { - "text": "이 페이지는 휴지통에 있습니다", - "restore": "페이지 복원", - "deletePermanent": "영구적으로 삭제", - "deletePermanentDescription": "이 페이지를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "text": "현재 페이지는 휴지통에 있습니다", + "restore": "페이지 복구", + "deletePermanent": "영구 삭제" }, "dialogCreatePageNameHint": "페이지 이름", "questionBubble": { - "shortcuts": "단축키", - "whatsNew": "새로운 기능", - "markdown": "Markdown", + "shortcuts": "바로 가기", + "whatsNew": "새로운 소식", + "help": "도움 및 지원", + "markdown": "가격 인하", "debug": { "name": "디버그 정보", - "success": "디버그 정보를 클립보드에 복사했습니다!", - "fail": "디버그 정보를 클립보드에 복사할 수 없습니다" + "success": "디버그 정보를 클립보드로 복사했습니다.", + "fail": "디버그 정보를 클립보드로 복사할 수 없습니다." }, - "feedback": "피드백", - "help": "도움말 및 지원" + "feedback": "피드백" }, "menuAppHeader": { - "moreButtonToolTip": "제거, 이름 변경 등...", - "addPageTooltip": "빠르게 페이지 추가", - "defaultNewPageName": "제목 없음", - "renameDialog": "이름 변경", - "pageNameSuffix": "복사본" + "addPageTooltip": "하위에 페이지 추가", + "defaultNewPageName": "제목없음", + "renameDialog": "이름변경" }, - "noPagesInside": "내부에 페이지가 없습니다", "toolbar": { - "undo": "실행 취소", - "redo": "다시 실행", + "undo": "실행취소", + "redo": "재실행", "bold": "굵게", "italic": "기울임꼴", "underline": "밑줄", "strike": "취소선", "numList": "번호 매기기 목록", "bulletList": "글머리 기호 목록", - "checkList": "체크리스트", + "checkList": "작업 목록", "inlineCode": "인라인 코드", - "quote": "인용 블록", + "quote": "인용구 블록", "header": "헤더", - "highlight": "강조", + "highlight": "하이라이트", "color": "색상", - "addLink": "링크 추가" + "addLink": "링크 추가", + "link": "링크" }, "tooltip": { - "lightMode": "라이트 모드로 전환", - "darkMode": "다크 모드로 전환", + "lightMode": "라이트 모드로 변경", + "darkMode": "다크 모드로 변경", "openAsPage": "페이지로 열기", - "addNewRow": "새 행 추가", - "openMenu": "메뉴 열기", - "dragRow": "행 순서 변경", + "addNewRow": "열 추가", + "openMenu": "메뉴를 여시려면 클릭하세요", + "dragRow": "행을 재정렬하려면 길게 누르세요.", "viewDataBase": "데이터베이스 보기", - "referencePage": "이 {name}이 참조됨", - "addBlockBelow": "아래에 블록 추가", - "aiGenerate": "생성" + "referencePage": "이 {name}은(는) 참조됩니다", + "addBlockBelow": "아래에 블록 추가" }, "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 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "aiResponseLimitDialogTitle": "AI 응답 한도에 도달했습니다", - "aiResponseLimit": "무료 AI 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max 또는 Pro 플랜을 클릭하여 더 많은 AI 응답을 받으세요", - "askOwnerToUpgradeToPro": "작업 공간의 무료 저장 공간이 부족합니다. 작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요", - "askOwnerToUpgradeToProIOS": "작업 공간의 무료 저장 공간이 부족합니다.", - "askOwnerToUpgradeToAIMax": "작업 공간의 무료 AI 응답이 부족합니다. 작업 공간 소유자에게 플랜을 업그레이드하거나 AI 애드온을 구매하도록 요청하세요", - "askOwnerToUpgradeToAIMaxIOS": "작업 공간의 무료 AI 응답이 부족합니다.", - "purchaseAIMax": "작업 공간의 AI 이미지 응답이 부족합니다. 작업 공간 소유자에게 AI Max를 구매하도록 요청하세요", - "aiImageResponseLimit": "AI 이미지 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max를 클릭하여 더 많은 AI 이미지 응답을 받으세요", - "purchaseStorageSpace": "저장 공간 구매", - "singleFileProPlanLimitationDescription": "무료 플랜에서 허용되는 최대 파일 업로드 크기를 초과했습니다. 더 큰 파일을 업로드하려면 Pro 플랜으로 업그레이드하세요", - "purchaseAIResponse": "구매 ", - "askOwnerToUpgradeToLocalAI": "작업 공간 소유자에게 AI On-device를 활성화하도록 요청하세요", - "upgradeToAILocal": "최고의 프라이버시를 위해 로컬 모델을 장치에서 실행", - "upgradeToAILocalDesc": "PDF와 채팅하고, 글쓰기를 개선하고, 로컬 AI를 사용하여 테이블을 자동으로 채우세요" + "openSidebar": "사이드바 열기" }, "notifications": { "export": { - "markdown": "노트를 Markdown으로 내보냈습니다", + "markdown": "마크다운으로 노트를 내보냄", "path": "Documents/flowy" } }, "contactsPage": { "title": "연락처", - "whatsHappening": "이번 주에 무슨 일이 있나요?", + "whatsHappening": "이번주에는 무슨 일이 있나요?", "addContact": "연락처 추가", - "editContact": "연락처 수정" + "editContact": "연락처 편집" }, "button": { "ok": "확인", - "confirm": "확인", "done": "완료", "cancel": "취소", "signIn": "로그인", "signOut": "로그아웃", "complete": "완료", "save": "저장", - "generate": "생성", + "generate": "생성하다", "esc": "ESC", - "keep": "유지", - "tryAgain": "다시 시도", - "discard": "버리기", - "replace": "교체", + "keep": "유지하다", + "tryAgain": "다시 시도하십시오", + "discard": "버리다", + "replace": "바꾸다", "insertBelow": "아래에 삽입", - "insertAbove": "위에 삽입", "upload": "업로드", - "edit": "편집", + "edit": "편집하다", "delete": "삭제", - "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": "원본 링크 복사" + "duplicate": "복제하다", + "putback": "다시 집어 넣어", + "share": "공유" }, "label": { "welcome": "환영합니다!", "firstName": "이름", "middleName": "중간 이름", "lastName": "성", - "stepX": "단계 {X}" + "stepX": "{X} 단계" }, "oAuth": { "err": { - "failedTitle": "계정에 연결할 수 없습니다.", - "failedMsg": "브라우저에서 로그인 프로세스를 완료했는지 확인하세요." + "failedTitle": "계정에 연결을 할 수 없습니다.", + "failedMsg": "브라우저에서 회원가입이 완료되었는지 확인해주세요." }, "google": { - "title": "GOOGLE 로그인", - "instruction1": "Google 연락처를 가져오려면 웹 브라우저를 사용하여 이 애플리케이션을 인증해야 합니다.", - "instruction2": "아이콘을 클릭하거나 텍스트를 선택하여 이 코드를 클립보드에 복사하세요:", - "instruction3": "웹 브라우저에서 다음 링크로 이동하고 위의 코드를 입력하세요:", - "instruction4": "가입을 완료했으면 아래 버튼을 누르세요:" + "title": "GOOGLE SIGN-IN", + "instruction1": "구글 연락처를 가져오기 위해서 웹브라우저로 앱을 승인 해야 합니다.", + "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": "네임스페이스는 최소 2자 이상이어야 합니다", - "onlyWorkspaceOwnerCanUpdateNamespace": "작업 공간 소유자만 네임스페이스를 업데이트할 수 있습니다", - "onlyWorkspaceOwnerCanRemoveHomepage": "작업 공간 소유자만 홈페이지를 제거할 수 있습니다", - "setHomepageFailed": "홈페이지 설정 실패", - "namespaceTooLong": "네임스페이스가 너무 깁니다. 다른 네임스페이스를 시도하세요", - "namespaceTooShort": "네임스페이스가 너무 짧습니다. 다른 네임스페이스를 시도하세요", - "namespaceIsReserved": "네임스페이스가 예약되어 있습니다. 다른 네임스페이스를 시도하세요", - "updatePathNameFailed": "경로 이름 업데이트 실패", - "removeHomePageFailed": "홈페이지 제거 실패", - "publishNameContainsInvalidCharacters": "경로 이름에 잘못된 문자가 포함되어 있습니다. 다른 이름을 시도하세요", - "publishNameTooShort": "경로 이름이 너무 짧습니다. 다른 이름을 시도하세요", - "publishNameTooLong": "경로 이름이 너무 깁니다. 다른 이름을 시도하세요", - "publishNameAlreadyInUse": "경로 이름이 이미 사용 중입니다. 다른 이름을 시도하세요", - "namespaceContainsInvalidCharacters": "네임스페이스에 잘못된 문자가 포함되어 있습니다. 다른 네임스페이스를 시도하세요", - "publishPermissionDenied": "작업 공간 소유자 또는 페이지 게시자만 게시 설정을 관리할 수 있습니다", - "publishNameCannotBeEmpty": "경로 이름은 비워둘 수 없습니다. 다른 이름을 시도하세요" - }, - "success": { - "namespaceUpdated": "네임스페이스가 성공적으로 업데이트되었습니다", - "setHomepageSuccess": "홈페이지가 성공적으로 설정되었습니다", - "updatePathNameSuccess": "경로 이름이 성공적으로 업데이트되었습니다", - "removeHomePageSuccess": "홈페이지가 성공적으로 제거되었습니다" - } - }, "accountPage": { - "menuLabel": "계정 및 앱", - "title": "내 계정", "general": { - "title": "계정 이름 및 프로필 이미지", + "title": "사용자 이름 & 프로필 사진", "changeProfilePicture": "프로필 사진 변경" - }, - "email": { - "title": "이메일", - "actions": { - "change": "이메일 변경" - } - }, - "login": { - "title": "계정 로그인", - "loginLabel": "로그인", - "logoutLabel": "로그아웃" - }, - "isUpToDate": "@:appName이 최신 상태입니다!", - "officialVersion": "버전 {version} (공식 빌드)" - }, - "workspacePage": { - "menuLabel": "작업 공간", - "title": "작업 공간", - "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜/시간 형식 및 언어를 사용자 정의합니다.", - "workspaceName": { - "title": "작업 공간 이름" - }, - "workspaceIcon": { - "title": "작업 공간 아이콘", - "description": "작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다." - }, - "appearance": { - "title": "외관", - "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.", - "options": { - "system": "자동", - "light": "라이트", - "dark": "다크" - } - }, - "resetCursorColor": { - "title": "문서 커서 색상 재설정", - "description": "커서 색상을 재설정하시겠습니까?" - }, - "resetSelectionColor": { - "title": "문서 선택 색상 재설정", - "description": "선택 색상을 재설정하시겠습니까?" - }, - "resetWidth": { - "resetSuccess": "문서 너비가 성공적으로 재설정되었습니다" - }, - "theme": { - "title": "테마", - "description": "미리 설정된 테마를 선택하거나 사용자 정의 테마를 업로드하세요.", - "uploadCustomThemeTooltip": "사용자 정의 테마 업로드" - }, - "workspaceFont": { - "title": "작업 공간 글꼴", - "noFontHint": "글꼴을 찾을 수 없습니다. 다른 용어를 시도하세요." - }, - "textDirection": { - "title": "텍스트 방향", - "leftToRight": "왼쪽에서 오른쪽으로", - "rightToLeft": "오른쪽에서 왼쪽으로", - "auto": "자동", - "enableRTLItems": "RTL 도구 모음 항목 활성화" - }, - "layoutDirection": { - "title": "레이아웃 방향", - "leftToRight": "왼쪽에서 오른쪽으로", - "rightToLeft": "오른쪽에서 왼쪽으로" - }, - "dateTime": { - "title": "날짜 및 시간", - "example": "{}에 {} ({})", - "24HourTime": "24시간 형식", - "dateFormat": { - "label": "날짜 형식", - "local": "로컬", - "us": "미국", - "iso": "ISO", - "friendly": "친숙한", - "dmy": "일/월/년" - } - }, - "language": { - "title": "언어" - }, - "deleteWorkspacePrompt": { - "title": "작업 공간 삭제", - "content": "이 작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다." - }, - "leaveWorkspacePrompt": { - "title": "작업 공간 나가기", - "content": "이 작업 공간을 나가시겠습니까? 모든 페이지와 데이터에 대한 액세스를 잃게 됩니다.", - "success": "작업 공간을 성공적으로 나갔습니다.", - "fail": "작업 공간 나가기 실패." - }, - "manageWorkspace": { - "title": "작업 공간 관리", - "leaveWorkspace": "작업 공간 나가기", - "deleteWorkspace": "작업 공간 삭제" } }, - "manageDataPage": { - "menuLabel": "데이터 관리", - "title": "데이터 관리", - "description": "로컬 저장소 데이터를 관리하거나 기존 데이터를 @:appName에 가져옵니다.", - "dataStorage": { - "title": "파일 저장 위치", - "tooltip": "파일이 저장되는 위치", - "actions": { - "change": "경로 변경", - "open": "폴더 열기", - "openTooltip": "현재 데이터 폴더 위치 열기", - "copy": "경로 복사", - "copiedHint": "경로가 복사되었습니다!", - "resetTooltip": "기본 위치로 재설정" - }, - "resetDialog": { - "title": "확실합니까?", - "description": "경로를 기본 데이터 위치로 재설정해도 데이터가 삭제되지 않습니다. 현재 데이터를 다시 가져오려면 현재 위치의 경로를 먼저 복사해야 합니다." - } - }, - "importData": { - "title": "데이터 가져오기", - "tooltip": "@:appName 백업/데이터 폴더에서 데이터 가져오기", - "description": "외부 @:appName 데이터 폴더에서 데이터 복사", - "action": "파일 찾아보기" - }, - "encryption": { - "title": "암호화", - "tooltip": "데이터 저장 및 암호화 방법 관리", - "descriptionNoEncryption": "암호화를 켜면 모든 데이터가 암호화됩니다. 이 작업은 되돌릴 수 없습니다.", - "descriptionEncrypted": "데이터가 암호화되었습니다.", - "action": "데이터 암호화", - "dialog": { - "title": "모든 데이터를 암호화하시겠습니까?", - "description": "모든 데이터를 암호화하면 데이터가 안전하고 보호됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?" - } - }, - "cache": { - "title": "캐시 지우기", - "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", - "dialog": { - "title": "캐시 지우기", - "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", - "successHint": "캐시가 지워졌습니다!" - } - }, - "data": { - "fixYourData": "데이터 수정", - "fixButton": "수정", - "fixYourDataDescription": "데이터에 문제가 있는 경우 여기에서 수정할 수 있습니다." - } - }, - "shortcutsPage": { - "menuLabel": "단축키", - "title": "단축키", - "editBindingHint": "새 바인딩 입력", - "searchHint": "검색", - "actions": { - "resetDefault": "기본값으로 재설정" - }, - "errorPage": { - "message": "단축키를 로드하지 못했습니다: {}", - "howToFix": "다시 시도해 보세요. 문제가 계속되면 GitHub에 문의하세요." - }, - "resetDialog": { - "title": "단축키 재설정", - "description": "모든 키 바인딩을 기본값으로 재설정합니다. 나중에 되돌릴 수 없습니다. 계속하시겠습니까?", - "buttonLabel": "재설정" - }, - "conflictDialog": { - "title": "{}가 이미 사용 중입니다", - "descriptionPrefix": "이 키 바인딩은 현재 ", - "descriptionSuffix": "에서 사용 중입니다. 이 키 바인딩을 교체하면 {}에서 제거됩니다.", - "confirmLabel": "계속" - }, - "editTooltip": "키 바인딩 편집을 시작하려면 누르세요", - "keybindings": { - "toggleToDoList": "할 일 목록 전환", - "insertNewParagraphInCodeblock": "새 단락 삽입", - "pasteInCodeblock": "코드 블록에 붙여넣기", - "selectAllCodeblock": "모두 선택", - "indentLineCodeblock": "줄 시작에 두 칸 삽입", - "outdentLineCodeblock": "줄 시작에서 두 칸 삭제", - "twoSpacesCursorCodeblock": "커서 위치에 두 칸 삽입", - "copy": "선택 항목 복사", - "paste": "내용 붙여넣기", - "cut": "선택 항목 잘라내기", - "alignLeft": "텍스트 왼쪽 정렬", - "alignCenter": "텍스트 가운데 정렬", - "alignRight": "텍스트 오른쪽 정렬", - "insertInlineMathEquation": "인라인 수학 방정식 삽입", - "undo": "실행 취소", - "redo": "다시 실행", - "convertToParagraph": "블록을 단락으로 변환", - "backspace": "삭제", - "deleteLeftWord": "왼쪽 단어 삭제", - "deleteLeftSentence": "왼쪽 문장 삭제", - "delete": "오른쪽 문자 삭제", - "deleteMacOS": "왼쪽 문자 삭제", - "deleteRightWord": "오른쪽 단어 삭제", - "moveCursorLeft": "커서를 왼쪽으로 이동", - "moveCursorBeginning": "커서를 시작으로 이동", - "moveCursorLeftWord": "커서를 왼쪽 단어로 이동", - "moveCursorLeftSelect": "선택하고 커서를 왼쪽으로 이동", - "moveCursorBeginSelect": "선택하고 커서를 시작으로 이동", - "moveCursorLeftWordSelect": "선택하고 커서를 왼쪽 단어로 이동", - "moveCursorRight": "커서를 오른쪽으로 이동", - "moveCursorEnd": "커서를 끝으로 이동", - "moveCursorRightWord": "커서를 오른쪽 단어로 이동", - "moveCursorRightSelect": "선택하고 커서를 오른쪽으로 이동", - "moveCursorEndSelect": "선택하고 커서를 끝으로 이동", - "moveCursorRightWordSelect": "선택하고 커서를 오른쪽 단어로 이동", - "moveCursorUp": "커서를 위로 이동", - "moveCursorTopSelect": "선택하고 커서를 위로 이동", - "moveCursorTop": "커서를 위로 이동", - "moveCursorUpSelect": "선택하고 커서를 위로 이동", - "moveCursorBottomSelect": "선택하고 커서를 아래로 이동", - "moveCursorBottom": "커서를 아래로 이동", - "moveCursorDown": "커서를 아래로 이동", - "moveCursorDownSelect": "선택하고 커서를 아래로 이동", - "home": "맨 위로 스크롤", - "end": "맨 아래로 스크롤", - "toggleBold": "굵게 전환", - "toggleItalic": "기울임꼴 전환", - "toggleUnderline": "밑줄 전환", - "toggleStrikethrough": "취소선 전환", - "toggleCode": "인라인 코드 전환", - "toggleHighlight": "강조 전환", - "showLinkMenu": "링크 메뉴 표시", - "openInlineLink": "인라인 링크 열기", - "openLinks": "선택한 모든 링크 열기", - "indent": "들여쓰기", - "outdent": "내어쓰기", - "exit": "편집 종료", - "pageUp": "한 페이지 위로 스크롤", - "pageDown": "한 페이지 아래로 스크롤", - "selectAll": "모두 선택", - "pasteWithoutFormatting": "서식 없이 붙여넣기", - "showEmojiPicker": "이모지 선택기 표시", - "enterInTableCell": "테이블에 줄 바꿈 추가", - "leftInTableCell": "테이블에서 왼쪽 셀로 이동", - "rightInTableCell": "테이블에서 오른쪽 셀로 이동", - "upInTableCell": "테이블에서 위쪽 셀로 이동", - "downInTableCell": "테이블에서 아래쪽 셀로 이동", - "tabInTableCell": "테이블에서 다음 사용 가능한 셀로 이동", - "shiftTabInTableCell": "테이블에서 이전 사용 가능한 셀로 이동", - "backSpaceInTableCell": "셀의 시작 부분에서 멈춤" - }, - "commands": { - "codeBlockNewParagraph": "코드 블록 옆에 새 단락 삽입", - "codeBlockIndentLines": "코드 블록에서 줄 시작에 두 칸 삽입", - "codeBlockOutdentLines": "코드 블록에서 줄 시작에서 두 칸 삭제", - "codeBlockAddTwoSpaces": "코드 블록에서 커서 위치에 두 칸 삽입", - "codeBlockSelectAll": "코드 블록 내의 모든 내용 선택", - "codeBlockPasteText": "코드 블록에 텍스트 붙여넣기", - "textAlignLeft": "텍스트를 왼쪽으로 정렬", - "textAlignCenter": "텍스트를 가운데로 정렬", - "textAlignRight": "텍스트를 오른쪽으로 정렬" - }, - "couldNotLoadErrorMsg": "단축키를 로드할 수 없습니다. 다시 시도하세요", - "couldNotSaveErrorMsg": "단축키를 저장할 수 없습니다. 다시 시도하세요" - }, "aiPage": { - "title": "AI 설정", - "menuLabel": "AI 설정", "keys": { - "enableAISearchTitle": "AI 검색", - "aiSettingsDescription": "선호하는 모델을 선택하여 AppFlowy AI를 구동하세요. 이제 GPT 4-o, Claude 3,5, Llama 3.1 및 Mistral 7B를 포함합니다", - "loginToEnableAIFeature": "AI 기능은 @:appName Cloud에 로그인한 후에만 활성화됩니다. @:appName 계정이 없는 경우 '내 계정'에서 가입하세요", - "llmModel": "언어 모델", - "llmModelType": "언어 모델 유형", - "downloadLLMPrompt": "{} 다운로드", - "downloadAppFlowyOfflineAI": "AI 오프라인 패키지를 다운로드하면 AI가 장치에서 실행됩니다. 계속하시겠습니까?", - "downloadLLMPromptDetail": "{} 로컬 모델을 다운로드하면 최대 {}의 저장 공간이 필요합니다. 계속하시겠습니까?", - "downloadBigFilePrompt": "다운로드 완료까지 약 10분이 소요될 수 있습니다", - "downloadAIModelButton": "다운로드", - "downloadingModel": "다운로드 중", - "localAILoaded": "로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다", - "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", - "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", - "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다", - "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", - "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", + "localAILoaded": "로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다.", + "localAIStart": "로컬 AI 대화 시작중...", + "localAILoading": "로컬 AI 대화 모델 로딩중...", + "localAIStopped": "로컬 AI가 중단되었습니다", "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", - "restartLocalAI": "로컬 AI 다시 시작", - "disableLocalAITitle": "로컬 AI 비활성화", - "disableLocalAIDescription": "로컬 AI를 비활성화하시겠습니까?", - "localAIToggleTitle": "로컬 AI를 활성화 또는 비활성화하려면 전환", - "offlineAIInstruction1": "다음을 따르세요", - "offlineAIInstruction2": "지침", - "offlineAIInstruction3": "오프라인 AI를 활성화하려면", - "offlineAIDownload1": "AppFlowy AI를 다운로드하지 않은 경우 먼저", - "offlineAIDownload2": "다운로드", - "offlineAIDownload3": "하세요", - "activeOfflineAI": "활성화됨", - "downloadOfflineAI": "다운로드", - "openModelDirectory": "폴더 열기", - "pleaseFollowThese": "지침", - "instructions": "이 지침을 따르세요", - "installOllamaLai": "Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요", - "startLocalAI": "로컬 AI를 시작하는 데 몇 초가 소요될 수 있습니다" + "restartLocalAI": "로컬 AI 재시작", + "disableLocalAIDescription": "로컬 AI를 비활성화하시겠습니까?" } }, "planPage": { - "menuLabel": "플랜", - "title": "가격 플랜", "planUsage": { - "title": "플랜 사용 요약", - "storageLabel": "저장 공간", - "storageUsage": "{} / {} GB", - "unlimitedStorageLabel": "무제한 저장 공간", - "collaboratorsLabel": "멤버", - "collaboratorsUsage": "{} / {}", - "aiResponseLabel": "AI 응답", - "aiResponseUsage": "{} / {}", - "unlimitedAILabel": "무제한 응답", - "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "Mac용 AI On-device", - "memberProToggle": "더 많은 멤버 및 무제한 AI", - "aiMaxToggle": "무제한 AI 및 고급 모델 액세스", - "aiOnDeviceToggle": "최고의 프라이버시를 위한 로컬 AI", - "aiCredit": { - "title": "@:appName AI 크레딧 추가", - "price": "{}", - "priceDescription": "1,000 크레딧당", - "purchase": "AI 구매", - "info": "작업 공간당 1,000개의 AI 크레딧을 추가하여 더 스마트하고 빠른 결과를 위해 AI를 워크플로에 원활하게 통합하세요:", - "infoItemOne": "데이터베이스당 최대 10,000개의 응답", - "infoItemTwo": "작업 공간당 최대 1,000개의 응답" - }, - "currentPlan": { - "bannerLabel": "현재 플랜", - "freeTitle": "무료", - "proTitle": "Pro", - "teamTitle": "팀", - "freeInfo": "모든 것을 정리하기 위한 최대 2명의 개인용", - "proInfo": "최대 10명의 소규모 팀을 위한 완벽한 솔루션.", - "teamInfo": "모든 생산적이고 잘 조직된 팀을 위한 완벽한 솔루션.", - "upgrade": "플랜 변경", - "canceledInfo": "플랜이 취소되었습니다. {}에 무료 플랜으로 다운그레이드됩니다." - }, - "addons": { - "title": "애드온", - "addLabel": "추가", - "activeLabel": "추가됨", - "aiMax": { - "title": "AI Max", - "description": "무제한 AI 응답 및 고급 AI 모델로 구동되는 50개의 AI 이미지를 매월 제공합니다", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별" - }, - "aiOnDevice": { - "title": "Mac용 AI On-device", - "description": "장치에서 Mistral 7B, LLAMA 3 및 기타 로컬 모델 실행", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별", - "recommend": "M1 이상 권장" - } - }, - "deal": { - "bannerLabel": "새해 할인!", - "title": "팀을 성장시키세요!", - "info": "Pro 및 팀 플랜을 업그레이드하고 10% 할인 혜택을 받으세요! @:appName AI를 포함한 강력한 새로운 기능으로 작업 공간 생산성을 높이세요.", - "viewPlans": "플랜 보기" - } - } - }, - "billingPage": { - "menuLabel": "청구", - "title": "청구", - "plan": { - "title": "플랜", - "freeLabel": "무료", - "proLabel": "Pro", - "planButtonLabel": "플랜 변경", - "billingPeriod": "청구 기간", - "periodButtonLabel": "기간 수정" - }, - "paymentDetails": { - "title": "결제 세부 정보", - "methodLabel": "결제 방법", - "methodButtonLabel": "방법 수정" - }, - "addons": { - "title": "애드온", - "addLabel": "추가", - "removeLabel": "제거", - "renewLabel": "갱신", - "aiMax": { - "label": "AI Max", - "description": "무제한 AI 및 고급 모델 잠금 해제", - "activeDescription": "다음 청구서가 {}에 만료됩니다", - "canceledDescription": "AI Max는 {}까지 사용할 수 있습니다" - }, - "aiOnDevice": { - "label": "Mac용 AI On-device", - "description": "장치에서 무제한 AI 잠금 해제", - "activeDescription": "다음 청구서가 {}에 만료됩니다", - "canceledDescription": "Mac용 AI On-device는 {}까지 사용할 수 있습니다" - }, - "removeDialog": { - "title": "{} 제거", - "description": "{plan}을 제거하시겠습니까? {plan}의 기능과 혜택에 대한 액세스를 즉시 잃게 됩니다." - } - }, - "currentPeriodBadge": "현재", - "changePeriod": "기간 변경", - "planPeriod": "{} 기간", - "monthlyInterval": "월별", - "monthlyPriceInfo": "월별 청구되는 좌석당", - "annualInterval": "연간", - "annualPriceInfo": "연간 청구되는 좌석당" - }, - "comparePlanDialog": { - "title": "플랜 비교 및 선택", - "planFeatures": "플랜\n기능", - "current": "현재", - "actions": { - "upgrade": "업그레이드", - "downgrade": "다운그레이드", - "current": "현재" - }, - "freePlan": { - "title": "무료", - "description": "모든 것을 정리하기 위한 최대 2명의 개인용", - "price": "{}", - "priceInfo": "영원히 무료" - }, - "proPlan": { - "title": "Pro", - "description": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별\n\n{} 월별 청구" - }, - "planLabels": { - "itemOne": "작업 공간", - "itemTwo": "멤버", - "itemThree": "저장 공간", - "itemFour": "실시간 협업", - "itemFive": "모바일 앱", - "itemSix": "AI 응답", - "itemSeven": "AI 이미지", - "itemFileUpload": "파일 업로드", - "customNamespace": "맞춤 네임스페이스", - "tooltipSix": "평생 동안 응답 수는 재설정되지 않습니다", - "intelligentSearch": "지능형 검색", - "tooltipSeven": "작업 공간의 URL 일부를 사용자 정의할 수 있습니다", - "customNamespaceTooltip": "맞춤 게시 사이트 URL" - }, - "freeLabels": { - "itemOne": "작업 공간당 청구", - "itemTwo": "최대 2명", - "itemThree": "5 GB", - "itemFour": "예", - "itemFive": "예", - "itemSix": "평생 10회", - "itemSeven": "평생 2회", - "itemFileUpload": "최대 7 MB", - "intelligentSearch": "지능형 검색" - }, - "proLabels": { - "itemOne": "작업 공간당 청구", - "itemTwo": "최대 10명", - "itemThree": "무제한", - "itemFour": "예", - "itemFive": "예", - "itemSix": "무제한", - "itemSeven": "월별 10개 이미지", - "itemFileUpload": "무제한", - "intelligentSearch": "지능형 검색" - }, - "paymentSuccess": { - "title": "이제 {} 플랜을 사용 중입니다!", - "description": "결제가 성공적으로 처리되었으며 플랜이 @:appName {}로 업그레이드되었습니다. 플랜 세부 정보를 플랜 페이지에서 확인할 수 있습니다" - }, - "downgradeDialog": { - "title": "플랜을 다운그레이드하시겠습니까?", - "description": "플랜을 다운그레이드하면 무료 플랜으로 돌아갑니다. 멤버는 이 작업 공간에 대한 액세스를 잃을 수 있으며 저장 공간 제한을 충족하기 위해 공간을 확보해야 할 수 있습니다.", - "downgradeLabel": "플랜 다운그레이드" + "aiOnDeviceToggle": "최고의 개인 정보 보호를 위한 로컬 AI" } }, "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": "불만족" + "answerFour": "로컬 AI 모델에 대한 액세스" } }, - "common": { - "uploadingFile": "파일 업로드 중입니다. 앱을 종료하지 마세요", - "uploadNotionSuccess": "Notion zip 파일이 성공적으로 업로드되었습니다. 가져오기가 완료되면 확인 이메일을 받게 됩니다", - "reset": "재설정" - }, "menu": { - "appearance": "외관", + "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 Cloud 셀프 호스팅", - "appFlowyCloudUrlCanNotBeEmpty": "클라우드 URL은 비워둘 수 없습니다", - "clickToCopy": "클립보드에 복사", - "selfHostStart": "서버가 없는 경우", - "selfHostContent": "문서", - "selfHostEnd": "를 참조하여 셀프 호스팅 서버를 설정하는 방법을 확인하세요", - "pleaseInputValidURL": "유효한 URL을 입력하세요", - "changeUrl": "셀프 호스팅 URL을 {}로 변경", - "cloudURLHint": "서버의 기본 URL을 입력하세요", - "webURLHint": "웹 서버의 기본 URL을 입력하세요", - "cloudWSURL": "웹소켓 URL", - "cloudWSURLHint": "서버의 웹소켓 주소를 입력하세요", - "restartApp": "재시작", - "restartAppTip": "변경 사항을 적용하려면 애플리케이션을 재시작하세요. 현재 계정에서 로그아웃될 수 있습니다.", - "changeServerTip": "서버를 변경한 후 변경 사항을 적용하려면 재시작 버튼을 클릭해야 합니다", - "enableEncryptPrompt": "이 비밀로 데이터를 보호하려면 암호화를 활성화하세요. 안전하게 보관하세요. 활성화 후에는 비활성화할 수 없습니다. 비밀을 잃어버리면 데이터를 복구할 수 없습니다. 복사하려면 클릭하세요", - "inputEncryptPrompt": "암호화 비밀을 입력하세요", - "clickToCopySecret": "비밀을 복사하려면 클릭", - "configServerSetting": "서버 설정 구성", - "configServerGuide": "`빠른 시작`을 선택한 후 `설정`으로 이동하여 \"클라우드 설정\"을 구성하세요.", - "inputTextFieldHint": "비밀", - "historicalUserList": "사용자 로그인 기록", - "historicalUserListTooltip": "이 목록에는 익명 계정이 표시됩니다. 계정을 클릭하여 세부 정보를 확인할 수 있습니다. 익명 계정은 '시작하기' 버튼을 클릭하여 생성됩니다", - "openHistoricalUser": "익명 계정을 열려면 클릭", - "customPathPrompt": "Google Drive와 같은 클라우드 동기화 폴더에 @:appName 데이터 폴더를 저장하면 위험이 발생할 수 있습니다. 이 폴더 내의 데이터베이스에 여러 위치에서 동시에 액세스하거나 수정하면 동기화 충돌 및 데이터 손상이 발생할 수 있습니다", - "importAppFlowyData": "외부 @:appName 폴더에서 데이터 가져오기", - "importingAppFlowyDataTip": "데이터 가져오는 중입니다. 앱을 종료하지 마세요", - "importAppFlowyDataDescription": "외부 @:appName 데이터 폴더에서 데이터를 복사하여 현재 AppFlowy 데이터 폴더에 가져옵니다", - "importSuccess": "성공적으로 @:appName 데이터 폴더를 가져왔습니다", - "importFailed": "@:appName 데이터 폴더 가져오기 실패", - "importGuide": "자세한 내용은 참조 문서를 확인하세요" - }, - "notifications": { - "enableNotifications": { - "label": "알림 활성화", - "hint": "로컬 알림이 나타나지 않도록 하려면 끄세요." - }, - "showNotificationsIcon": { - "label": "알림 아이콘 표시", - "hint": "사이드바에서 알림 아이콘을 숨기려면 끄세요." - }, - "archiveNotifications": { - "allSuccess": "모든 알림이 성공적으로 보관되었습니다", - "success": "알림이 성공적으로 보관되었습니다" - }, - "markAsReadNotifications": { - "allSuccess": "모두 읽음으로 표시되었습니다", - "success": "읽음으로 표시되었습니다" - }, - "action": { - "markAsRead": "읽음으로 표시", - "multipleChoice": "더 선택", - "archive": "보관" - }, - "settings": { - "settings": "설정", - "markAllAsRead": "모두 읽음으로 표시", - "archiveAll": "모두 보관" - }, - "emptyInbox": { - "title": "받은 편지함 비어 있음!", - "description": "알림을 받으려면 알림을 설정하세요." - }, - "emptyUnread": { - "title": "읽지 않은 알림 없음", - "description": "모든 알림을 확인했습니다!" - }, - "emptyArchived": { - "title": "보관된 항목 없음", - "description": "보관된 알림이 여기에 표시됩니다." - }, - "tabs": { - "inbox": "받은 편지함", - "unread": "읽지 않음", - "archived": "보관됨" - }, - "refreshSuccess": "알림이 성공적으로 새로고침되었습니다", - "titles": { - "notifications": "알림", - "reminder": "알림" - } + "open": "설정 열기" }, "appearance": { - "resetSetting": "재설정", "fontFamily": { - "label": "글꼴", - "search": "검색", - "defaultFont": "시스템" + "label": "글꼴 패밀리", + "search": "검색" }, "themeMode": { "label": "테마 모드", "light": "라이트 모드", "dark": "다크 모드", - "system": "시스템에 맞춤" - }, - "fontScaleFactor": "글꼴 크기 비율", - "displaySize": "디스플레이 크기", - "documentSettings": { - "cursorColor": "문서 커서 색상", - "selectionColor": "문서 선택 색상", - "width": "문서 너비", - "changeWidth": "변경", - "pickColor": "색상 선택", - "colorShade": "색상 음영", - "opacity": "불투명도", - "hexEmptyError": "16진수 색상은 비워둘 수 없습니다", - "hexLengthError": "16진수 값은 6자리여야 합니다", - "hexInvalidError": "잘못된 16진수 값", - "opacityEmptyError": "불투명도는 비워둘 수 없습니다", - "opacityRangeError": "불투명도는 1에서 100 사이여야 합니다", - "app": "앱", - "flowy": "Flowy", - "apply": "적용" - }, - "layoutDirection": { - "label": "레이아웃 방향", - "hint": "화면의 콘텐츠 흐름을 왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽으로 제어합니다.", - "ltr": "LTR", - "rtl": "RTL" - }, - "textDirection": { - "label": "기본 텍스트 방향", - "hint": "텍스트가 기본적으로 왼쪽에서 시작할지 오른쪽에서 시작할지 지정합니다.", - "ltr": "LTR", - "rtl": "RTL", - "auto": "자동", - "fallback": "레이아웃 방향과 동일" + "system": "시스템에 적응" }, "themeUpload": { "button": "업로드", - "uploadTheme": "테마 업로드", - "description": "아래 버튼을 사용하여 사용자 정의 @:appName 테마를 업로드하세요.", - "loading": "테마를 검증하고 업로드하는 동안 기다려주세요...", - "uploadSuccess": "테마가 성공적으로 업로드되었습니다", - "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보세요.", + "description": "아래 버튼을 사용하여 나만의 @:appName 테마를 업로드하세요.", + "loading": "테마를 확인하고 업로드하는 동안 잠시 기다려 주십시오...", + "uploadSuccess": "테마가 성공적으로 업로드되었습니다.", + "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보십시오.", "filePickerDialogTitle": ".flowy_plugin 파일 선택", - "urlUploadFailure": "URL을 열지 못했습니다: {}" + "urlUploadFailure": "URL을 열지 못했습니다: {}", + "failure": "업로드된 테마의 형식이 잘못되었습니다." }, - "theme": "테마", + "theme": "주제", "builtInsLabel": "내장 테마", "pluginsLabel": "플러그인", - "dateFormat": { - "label": "날짜 형식", - "local": "로컬", - "us": "미국", - "iso": "ISO", - "friendly": "친숙한", - "dmy": "일/월/년" - }, - "timeFormat": { - "label": "시간 형식", - "twelveHour": "12시간 형식", - "twentyFourHour": "24시간 형식" - }, - "showNamingDialogWhenCreatingPage": "페이지 생성 시 이름 지정 대화 상자 표시", - "enableRTLToolbarItems": "RTL 도구 모음 항목 활성화", - "members": { - "title": "멤버 설정", - "inviteMembers": "멤버 초대", - "inviteHint": "이메일로 초대", - "sendInvite": "초대 보내기", - "copyInviteLink": "초대 링크 복사", - "label": "멤버", - "user": "사용자", - "role": "역할", - "removeFromWorkspace": "작업 공간에서 제거", - "removeFromWorkspaceSuccess": "작업 공간에서 성공적으로 제거되었습니다", - "removeFromWorkspaceFailed": "작업 공간에서 제거 실패", - "owner": "소유자", - "guest": "게스트", - "member": "멤버", - "memberHintText": "멤버는 페이지를 읽고 편집할 수 있습니다", - "guestHintText": "게스트는 페이지를 읽고, 반응하고, 댓글을 달 수 있으며, 권한이 있는 특정 페이지를 편집할 수 있습니다.", - "emailInvalidError": "잘못된 이메일입니다. 확인하고 다시 시도하세요", - "emailSent": "이메일이 전송되었습니다. 받은 편지함을 확인하세요", - "members": "멤버", - "membersCount": { - "zero": "{}명의 멤버", - "one": "{}명의 멤버", - "other": "{}명의 멤버" - }, - "inviteFailedDialogTitle": "초대 전송 실패", - "inviteFailedMemberLimit": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 업그레이드하세요.", - "inviteFailedMemberLimitMobile": "작업 공간의 멤버 한도에 도달했습니다.", - "memberLimitExceeded": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 ", - "memberLimitExceededUpgrade": "업그레이드", - "memberLimitExceededPro": "멤버 한도에 도달했습니다. 더 많은 멤버가 필요하면 ", - "memberLimitExceededProContact": "support@appflowy.io에 문의하세요", - "failedToAddMember": "멤버 추가 실패", - "addMemberSuccess": "멤버가 성공적으로 추가되었습니다", - "removeMember": "멤버 제거", - "areYouSureToRemoveMember": "이 멤버를 제거하시겠습니까?", - "inviteMemberSuccess": "초대가 성공적으로 전송되었습니다", - "failedToInviteMember": "멤버 초대 실패", - "workspaceMembersError": "문제가 발생했습니다", - "workspaceMembersErrorDescription": "현재 멤버 목록을 로드할 수 없습니다. 나중에 다시 시도하세요" - } + "lightLabel": "라이트 모드", + "darkLabel": "다크 모드" }, "files": { "copy": "복사", "defaultLocation": "파일 및 데이터 저장 위치 읽기", "exportData": "데이터 내보내기", - "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요", + "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요.", "restoreLocation": "@:appName 기본 경로로 복원", "customizeLocation": "다른 폴더 열기", - "restartApp": "변경 사항을 적용하려면 앱을 재시작하세요.", + "restartApp": "변경 사항을 적용하려면 앱을 다시 시작하십시오.", "exportDatabase": "데이터베이스 내보내기", - "selectFiles": "내보낼 파일 선택", + "selectFiles": "내보낼 파일을 선택하십시오", "selectAll": "모두 선택", - "deselectAll": "모두 선택 해제", + "deselectAll": "모두 선택 취소", "createNewFolder": "새 폴더 만들기", - "createNewFolderDesc": "데이터를 저장할 위치를 알려주세요", + "createNewFolderDesc": "데이터를 저장할 위치를 알려주십시오.", "defineWhereYourDataIsStored": "데이터가 저장되는 위치 정의", - "open": "열기", + "open": "열려 있는", "openFolder": "기존 폴더 열기", - "openFolderDesc": "기존 @:appName 폴더를 읽고 쓰기", + "openFolderDesc": "기존 @:appName 폴더에서 읽고 쓰기", "folderHintText": "폴더 이름", "location": "새 폴더 만들기", - "locationDesc": "@:appName 데이터 폴더의 이름을 지정하세요", - "browser": "찾아보기", - "create": "생성", - "set": "설정", + "locationDesc": "@:appName 데이터 폴더의 이름을 선택하세요", + "browser": "검색", + "create": "만들다", + "set": "세트", "folderPath": "폴더를 저장할 경로", - "locationCannotBeEmpty": "경로는 비워둘 수 없습니다", + "locationCannotBeEmpty": "경로는 비워둘 수 없습니다.", "pathCopiedSnackbar": "파일 저장 경로가 클립보드에 복사되었습니다!", "changeLocationTooltips": "데이터 디렉토리 변경", - "change": "변경", + "change": "변화", "openLocationTooltips": "다른 데이터 디렉토리 열기", "openCurrentDataFolder": "현재 데이터 디렉토리 열기", - "recoverLocationTooltips": "@:appName의 기본 데이터 디렉토리로 재설정", - "exportFileSuccess": "파일이 성공적으로 내보내졌습니다!", + "recoverLocationTooltips": "@:appName의 기본 데이터 디렉터리로 재설정", + "exportFileSuccess": "파일을 성공적으로 내보냈습니다!", "exportFileFail": "파일 내보내기 실패!", - "export": "내보내기", - "clearCache": "캐시 지우기", - "clearCacheDesc": "이미지가 로드되지 않거나 글꼴이 제대로 표시되지 않는 등의 문제가 발생하면 캐시를 지워보세요. 이 작업은 사용자 데이터에는 영향을 미치지 않습니다.", - "areYouSureToClearCache": "캐시를 지우시겠습니까?", - "clearCacheSuccess": "캐시가 성공적으로 지워졌습니다!" + "export": "내보내다" }, "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": "버전" + "selectAnIcon": "아이콘을 선택하세요", + "pleaseInputYourOpenAIKey": "AI 키를 입력하십시오" } }, "grid": { "deleteView": "이 보기를 삭제하시겠습니까?", - "createView": "새로 만들기", - "title": { - "placeholder": "제목 없음" - }, + "createView": "새로운", "settings": { "filter": "필터", - "sort": "정렬", + "sort": "종류", "sortBy": "정렬 기준", "properties": "속성", - "reorderPropertiesTooltip": "속성 순서 변경", + "reorderPropertiesTooltip": "드래그하여 속성 재정렬", "group": "그룹", "addFilter": "필터 추가", "deleteFilter": "필터 삭제", - "filterBy": "필터 기준", - "typeAValue": "값 입력...", - "layout": "레이아웃", - "compactMode": "압축 모드", - "databaseLayout": "레이아웃", - "viewList": { - "zero": "0개의 보기", - "one": "{count}개의 보기", - "other": "{count}개의 보기" - }, - "editView": "보기 편집", - "boardSettings": "보드 설정", - "calendarSettings": "캘린더 설정", - "createView": "새 보기", - "duplicateView": "보기 복제", - "deleteView": "보기 삭제", - "numberOfVisibleFields": "{}개 표시됨" - }, - "filter": { - "empty": "활성 필터 없음", - "addFilter": "필터 추가", - "cannotFindCreatableField": "필터링할 적절한 필드를 찾을 수 없습니다", - "conditon": "조건", - "where": "조건" + "filterBy": "필터링 기준...", + "typeAValue": "값을 입력하세요...", + "layout": "공들여 나열한 것", + "databaseLayout": "공들여 나열한 것", + "Properties": "속성" }, "textFilter": { "contains": "포함", - "doesNotContain": "포함하지 않음", - "endsWith": "끝남", + "doesNotContain": "포함되어 있지 않다", + "endsWith": "로 끝나다", "startWith": "시작", - "is": "일치", - "isNot": "일치하지 않음", - "isEmpty": "비어 있음", + "is": "~이다", + "isNot": "아니다", + "isEmpty": "비었다", "isNotEmpty": "비어 있지 않음", "choicechipPrefix": { - "isNot": "일치하지 않음", + "isNot": "아니다", "startWith": "시작", - "endWith": "끝남", - "isEmpty": "비어 있음", - "isNotEmpty": "비어 있지 않음" + "endWith": "로 끝나다", + "isEmpty": "비었다", + "isNotEmpty": "비어있지 않다" } }, "checkboxFilter": { - "isChecked": "체크됨", - "isUnchecked": "체크되지 않음", + "isChecked": "체크", + "isUnchecked": "체크 해제", "choicechipPrefix": { - "is": "체크됨" + "is": "~이다" } }, "checklistFilter": { - "isComplete": "완료됨", - "isIncomplted": "미완료" + "isComplete": "완료되었습니다", + "isIncomplted": "불완전하다" }, "selectOptionFilter": { - "is": "일치", - "isNot": "일치하지 않음", + "is": "~이다", + "isNot": "아니다", "contains": "포함", - "doesNotContain": "포함하지 않음", - "isEmpty": "비어 있음", - "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": "비어 있음", + "doesNotContain": "포함되어 있지 않다", + "isEmpty": "비었다", "isNotEmpty": "비어 있지 않음" }, "field": { - "label": "속성", - "hide": "속성 숨기기", - "show": "속성 표시", - "insertLeft": "왼쪽에 삽입", - "insertRight": "오른쪽에 삽입", + "hide": "숨기기", + "insertLeft": "왼쪽 삽입", + "insertRight": "오른쪽 삽입", "duplicate": "복제", "delete": "삭제", - "wrapCellContent": "텍스트 줄 바꿈", - "clear": "셀 지우기", - "switchPrimaryFieldTooltip": "기본 필드의 필드 유형을 변경할 수 없습니다", "textFieldName": "텍스트", "checkboxFieldName": "체크박스", "dateFieldName": "날짜", - "updatedAtFieldName": "마지막 수정", - "createdAtFieldName": "생성일", + "updatedAtFieldName": "마지막 수정 시간", + "createdAtFieldName": "만든 시간", "numberFieldName": "숫자", "singleSelectFieldName": "선택", - "multiSelectFieldName": "다중 선택", - "urlFieldName": "URL", + "multiSelectFieldName": "다중선택", + "urlFieldName": "링크", "checklistFieldName": "체크리스트", - "relationFieldName": "관계", - "summaryFieldName": "AI 요약", - "timeFieldName": "시간", - "mediaFieldName": "파일 및 미디어", - "translateFieldName": "AI 번역", - "translateTo": "번역 대상", "numberFormat": "숫자 형식", "dateFormat": "날짜 형식", - "includeTime": "시간 포함", - "isRange": "종료 날짜", + "includeTime": "시간 표시", "dateFormatFriendly": "월 일, 년", "dateFormatISO": "년-월-일", "dateFormatLocal": "월/일/년", @@ -1527,558 +419,181 @@ "dateFormatDayMonthYear": "일/월/년", "timeFormat": "시간 형식", "invalidTimeFormat": "잘못된 형식", - "timeFormatTwelveHour": "12시간", - "timeFormatTwentyFourHour": "24시간", - "clearDate": "날짜 지우기", - "dateTime": "날짜 시간", - "startDateTime": "시작 날짜 시간", - "endDateTime": "종료 날짜 시간", - "failedToLoadDate": "날짜 값을 로드하지 못했습니다", - "selectTime": "시간 선택", - "selectDate": "날짜 선택", - "visibility": "가시성", - "propertyType": "속성 유형", + "timeFormatTwelveHour": "12 시간", + "timeFormatTwentyFourHour": "24 시간", "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": "더 많은 행 작업" + "newProperty": "열 추가", + "deleteFieldPromptMessage": "해당 속성을 삭제 하시겠습니까?" }, "sort": { "ascending": "오름차순", "descending": "내림차순", - "by": "기준", - "empty": "활성 정렬 없음", - "cannotFindCreatableField": "정렬할 적절한 필드를 찾을 수 없습니다", - "deleteAllSorts": "모든 정렬 삭제", "addSort": "정렬 추가", - "sortsActive": "정렬 중에는 {intention}할 수 없습니다", - "removeSorting": "이 보기의 모든 정렬을 제거하고 계속하시겠습니까?", - "fieldInUse": "이미 이 필드로 정렬 중입니다" + "deleteSort": "정렬 삭제" }, "row": { - "label": "행", "duplicate": "복제", "delete": "삭제", - "titlePlaceholder": "제목 없음", - "textPlaceholder": "비어 있음", - "copyProperty": "속성이 클립보드에 복사되었습니다", + "textPlaceholder": "비어있음", + "copyProperty": "속성이 클립보드로 복사됨", "count": "개수", - "newRow": "새 행", - "loadMore": "더 로드", - "action": "작업", - "add": "아래에 추가하려면 클릭", - "drag": "이동하려면 드래그", - "deleteRowPrompt": "이 행을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "deleteCardPrompt": "이 카드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "dragAndClick": "이동하려면 드래그, 메뉴를 열려면 클릭", - "insertRecordAbove": "위에 레코드 삽입", - "insertRecordBelow": "아래에 레코드 삽입", - "noContent": "내용 없음", - "reorderRowDescription": "행 순서 변경", - "createRowAboveDescription": "위에 행 생성", - "createRowBelowDescription": "아래에 행 삽입" + "newRow": "행 추가", + "action": "행동" }, "selectOption": { "create": "생성", "purpleColor": "보라색", - "pinkColor": "분홍색", - "lightPinkColor": "연분홍색", - "orangeColor": "주황색", - "yellowColor": "노란색", + "pinkColor": "핑크색", + "lightPinkColor": "연한 핑크색", + "orangeColor": "오렌지색", + "yellowColor": "노랑색", "limeColor": "라임색", - "greenColor": "녹색", - "aquaColor": "청록색", - "blueColor": "파란색", + "greenColor": "초록색", + "aquaColor": "아쿠아색", + "blueColor": "파랑색", "deleteTag": "태그 삭제", "colorPanelTitle": "색상", "panelTitle": "옵션 선택 또는 생성", - "searchOption": "옵션 검색", - "searchOrCreateOption": "옵션 검색 또는 생성", - "createNew": "새로 생성", - "orSelectOne": "또는 옵션 선택", - "typeANewOption": "새 옵션 입력", - "tagName": "태그 이름" + "searchOption": "옵션 검색" }, "checklist": { - "taskHint": "작업 설명", - "addNew": "새 작업 추가", - "submitNewTask": "생성", - "hideComplete": "완료된 작업 숨기기", - "showComplete": "모든 작업 표시" - }, - "url": { - "launch": "브라우저에서 링크 열기", - "copy": "링크를 클립보드에 복사", - "textFieldHint": "URL 입력" - }, - "relation": { - "relatedDatabasePlaceLabel": "관련 데이터베이스", - "relatedDatabasePlaceholder": "없음", - "inRelatedDatabase": "에", - "rowSearchTextFieldPlaceholder": "검색", - "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 아래 목록에서 하나를 먼저 선택하세요:", - "emptySearchResult": "레코드를 찾을 수 없습니다", - "linkedRowListLabel": "{count}개의 연결된 행", - "unlinkedRowListLabel": "다른 행 연결" + "addNew": "항목 추가" }, "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": "파일 링크 삽입" - } + "referencedGridPrefix": "관점" }, "document": { "menuName": "문서", "date": { - "timeHintTextInTwelveHour": "오후 01:00", + "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "생성 중...", "slashMenu": { "board": { "selectABoardToLinkTo": "연결할 보드 선택", - "createANewBoard": "새 보드 생성" + "createANewBoard": "새 보드 만들기" }, "grid": { "selectAGridToLinkTo": "연결할 그리드 선택", - "createANewGrid": "새 그리드 생성" + "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": "2열", - "threeColumns": "3열", - "fourColumns": "4열" - }, - "subPage": { - "name": "문서", - "keyword1": "하위 페이지", - "keyword2": "페이지", - "keyword3": "자식 페이지", - "keyword4": "페이지 삽입", - "keyword5": "페이지 포함", - "keyword6": "새 페이지", - "keyword7": "페이지 생성", - "keyword8": "문서" + "createANewCalendar": "새 캘린더 만들기" } }, "selectionMenu": { - "outline": "개요", - "codeBlock": "코드 블록" + "outline": "개요" }, "plugins": { - "referencedBoard": "참조된 보드", + "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에게 물어보기", + "referencedCalendar": "참조된 달력", + "autoGeneratorMenuItemName": "AI 작성자", + "autoGeneratorTitleName": "AI: AI에게 무엇이든 쓰라고 요청하세요...", + "autoGeneratorLearnMore": "더 알아보기", + "autoGeneratorGenerate": "생성하다", + "autoGeneratorHintText": "OpenAI에게 물어보세요 ...", + "autoGeneratorCantGetOpenAIKey": "AI 키를 가져올 수 없습니다.", + "autoGeneratorRewrite": "고쳐 쓰기", + "smartEdit": "AI 어시스턴트", "aI": "AI", - "smartEditFixSpelling": "맞춤법 및 문법 수정", + "smartEditFixSpelling": "맞춤법 수정", "warning": "⚠️ AI 응답은 부정확하거나 오해의 소지가 있을 수 있습니다.", - "smartEditSummarize": "요약", - "smartEditImproveWriting": "글쓰기 개선", - "smartEditMakeLonger": "길게 만들기", - "smartEditCouldNotFetchResult": "AI에서 결과를 가져올 수 없습니다", - "smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다", + "smartEditSummarize": "요약하다", + "smartEditImproveWriting": "쓰기 향상", + "smartEditMakeLonger": "더 길게", + "smartEditCouldNotFetchResult": "OpenAI에서 결과를 가져올 수 없습니다.", + "smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다.", "smartEditDisabled": "설정에서 AI 연결", - "appflowyAIEditDisabled": "AI 기능을 활성화하려면 로그인하세요", - "discardResponse": "AI 응답을 버리시겠습니까?", - "createInlineMathEquation": "방정식 생성", - "fonts": "글꼴", - "insertDate": "날짜 삽입", - "emoji": "이모지", + "discardResponse": "AI 응답을 삭제하시겠습니까?", + "createInlineMathEquation": "방정식 만들기", "toggleList": "토글 목록", - "emptyToggleHeading": "빈 토글 h{}. 내용을 추가하려면 클릭하세요.", - "emptyToggleList": "빈 토글 목록. 내용을 추가하려면 클릭하세요.", - "emptyToggleHeadingWeb": "빈 토글 h{level}. 내용을 추가하려면 클릭하세요", - "quoteList": "인용 목록", - "numberedList": "번호 매기기 목록", - "bulletedList": "글머리 기호 목록", - "todoList": "할 일 목록", - "callout": "콜아웃", - "simpleTable": { - "moreActions": { - "color": "색상", - "align": "정렬", - "delete": "삭제", - "duplicate": "복제", - "insertLeft": "왼쪽에 삽입", - "insertRight": "오른쪽에 삽입", - "insertAbove": "위에 삽입", - "insertBelow": "아래에 삽입", - "headerColumn": "헤더 열", - "headerRow": "헤더 행", - "clearContents": "내용 지우기", - "setToPageWidth": "페이지 너비로 설정", - "distributeColumnsWidth": "열 너비 균등 분배", - "duplicateRow": "행 복제", - "duplicateColumn": "열 복제", - "textColor": "텍스트 색상", - "cellBackgroundColor": "셀 배경 색상", - "duplicateTable": "테이블 복제" - }, - "clickToAddNewRow": "새 행을 추가하려면 클릭", - "clickToAddNewColumn": "새 열을 추가하려면 클릭", - "clickToAddNewRowAndColumn": "새 행과 열을 추가하려면 클릭", - "headerName": { - "table": "테이블", - "alignText": "텍스트 정렬" - } - }, "cover": { - "changeCover": "표지 변경", - "colors": "색상", + "changeCover": "커버를 바꾸다", + "colors": "그림 물감", "images": "이미지", "clearAll": "모두 지우기", - "abstract": "추상", + "abstract": "추상적인", "addCover": "표지 추가", "addLocalImage": "로컬 이미지 추가", "invalidImageUrl": "잘못된 이미지 URL", - "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다", + "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다.", "enterImageUrl": "이미지 URL 입력", - "add": "추가", - "back": "뒤로", + "add": "추가하다", + "back": "뒤쪽에", "saveToGallery": "갤러리에 저장", "removeIcon": "아이콘 제거", - "removeCover": "표지 제거", "pasteImageUrl": "이미지 URL 붙여넣기", "or": "또는", "pickFromFiles": "파일에서 선택", - "couldNotFetchImage": "이미지를 가져올 수 없습니다", + "couldNotFetchImage": "이미지를 가져올 수 없습니다.", "imageSavingFailed": "이미지 저장 실패", "addIcon": "아이콘 추가", - "changeIcon": "아이콘 변경", "coverRemoveAlert": "삭제 후 표지에서 제거됩니다.", - "alertDialogConfirmation": "계속하시겠습니까?" + "alertDialogConfirmation": "너 정말 계속하고 싶니?" }, "mathEquation": { - "name": "수학 방정식", - "addMathEquation": "TeX 방정식 추가", + "addMathEquation": "수학 방정식 추가", "editMathEquation": "수학 방정식 편집" }, "optionAction": { - "click": "클릭", + "click": "딸깍 하는 소리", "toOpenMenu": " 메뉴 열기", - "drag": "드래그", - "toMove": " 이동", "delete": "삭제", - "duplicate": "복제", - "turnInto": "변환", - "moveUp": "위로 이동", + "duplicate": "복제하다", + "turnInto": "로 변하다", + "moveUp": "이동", "moveDown": "아래로 이동", "color": "색상", - "align": "정렬", + "align": "맞추다", "left": "왼쪽", - "center": "가운데", + "center": "센터", "right": "오른쪽", - "defaultColor": "기본", - "depth": "깊이", - "copyLinkToBlock": "블록 링크 복사" + "defaultColor": "기본" }, "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": "링크로 변환" + "copiedToPasteBoard": "이미지 링크가 클립보드에 복사되었습니다." }, "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입니다. URL을 확인하고 다시 시도하세요.", - "networkAction": "삽입", - "fileTooBigError": "파일 크기가 너무 큽니다. 10MB 미만의 파일을 업로드하세요", - "renameFile": { - "title": "파일 이름 변경", - "description": "이 파일의 새 이름을 입력하세요", - "nameEmptyError": "파일 이름은 비워둘 수 없습니다." - }, - "uploadedAt": "{}에 업로드됨", - "linkedAt": "{}에 링크 추가됨", - "failedToOpenMsg": "열지 못했습니다. 파일을 찾을 수 없습니다" - }, - "subPage": { - "handlingPasteHint": " - (붙여넣기 처리 중)", - "errors": { - "failedDeletePage": "페이지 삭제 실패", - "failedCreatePage": "페이지 생성 실패", - "failedMovePage": "이 문서로 페이지 이동 실패", - "failedDuplicatePage": "페이지 복제 실패", - "failedDuplicateFindView": "페이지 복제 실패 - 원본 보기를 찾을 수 없습니다" - } - }, - "cannotMoveToItsChildren": "자식으로 이동할 수 없습니다" - }, - "outlineBlock": { - "placeholder": "목차" + "addHeadingToCreateOutline": "제목을 추가하여 목차를 만듭니다." + } }, "textBlock": { - "placeholder": "명령어를 입력하려면 '/'를 입력하세요" + "placeholder": "명령에 '/' 입력" }, "title": { - "placeholder": "제목 없음" + "placeholder": "무제" }, "imageBlock": { - "placeholder": "이미지 추가하려면 클릭", + "placeholder": "이미지를 추가하려면 클릭하세요.", "upload": { "label": "업로드", - "placeholder": "이미지 업로드하려면 클릭" + "placeholder": "이미지를 업로드하려면 클릭하세요." }, "url": { "label": "이미지 URL", "placeholder": "이미지 URL 입력" }, - "ai": { - "label": "AI로 이미지 생성", - "placeholder": "AI가 이미지를 생성할 프롬프트를 입력하세요" - }, - "stability_ai": { - "label": "Stability AI로 이미지 생성", - "placeholder": "Stability AI가 이미지를 생성할 프롬프트를 입력하세요" - }, "support": "이미지 크기 제한은 5MB입니다. 지원되는 형식: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "잘못된 이미지", - "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다", - "invalidImageFormat": "지원되지 않는 이미지 형식입니다. 지원되는 형식: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "잘못된 이미지 URL", - "noImage": "파일 또는 디렉토리가 없습니다", - "multipleImagesFailed": "하나 이상의 이미지 업로드 실패, 다시 시도하세요" - }, - "embedLink": { - "label": "링크 삽입", - "placeholder": "이미지 링크를 붙여넣거나 입력하세요" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "이미지 검색", - "pleaseInputYourOpenAIKey": "설정 페이지에서 AI 키를 입력하세요", - "saveImageToGallery": "이미지 저장", - "failedToAddImageToGallery": "이미지 저장 실패", - "successToAddImageToGallery": "이미지가 사진에 저장되었습니다", - "unableToLoadImage": "이미지를 로드할 수 없습니다", - "maximumImageSize": "최대 지원 업로드 이미지 크기는 10MB입니다", - "uploadImageErrorImageSizeTooBig": "이미지 크기는 10MB 미만이어야 합니다", - "imageIsUploading": "이미지 업로드 중", - "openFullScreen": "전체 화면으로 열기", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "이전 이미지", - "nextImageTooltip": "다음 이미지", - "zoomOutTooltip": "축소", - "zoomInTooltip": "확대", - "changeZoomLevelTooltip": "확대/축소 수준 변경", - "openLocalImage": "이미지 열기", - "downloadImage": "이미지 다운로드", - "closeViewer": "인터랙티브 뷰어 닫기", - "scalePercentage": "{}%", - "deleteImageTooltip": "이미지 삭제" - } + "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다.", + "invalidImageFormat": "이미지 형식은 지원되지 않습니다. 지원되는 형식: JPEG, PNG, GIF, SVG", + "invalidImageUrl": "잘못된 이미지 URL" } }, "codeBlock": { "language": { "label": "언어", - "placeholder": "언어 선택", - "auto": "자동" - }, - "copyTooltip": "복사", - "searchLanguageHint": "언어 검색", - "codeCopiedSnackbar": "코드가 클립보드에 복사되었습니다!" + "placeholder": "언어 선택" + } }, "inlineLink": { - "placeholder": "링크를 붙여넣거나 입력하세요", - "openInNewTab": "새 탭에서 열기", - "copyLink": "링크 복사", - "removeLink": "링크 제거", + "placeholder": "링크 붙여넣기 또는 입력", "url": { "label": "링크 URL", "placeholder": "링크 URL 입력" @@ -2087,1101 +602,89 @@ "label": "링크 제목", "placeholder": "링크 제목 입력" } - }, - "mention": { - "placeholder": "사람, 페이지 또는 날짜 언급...", - "page": { - "label": "페이지로 연결", - "tooltip": "페이지 열기" - }, - "deleted": "삭제됨", - "deletedContent": "이 콘텐츠는 존재하지 않거나 삭제되었습니다", - "noAccess": "액세스 불가", - "deletedPage": "삭제된 페이지", - "trashHint": " - 휴지통에 있음", - "morePages": "더 많은 페이지" - }, - "toolbar": { - "resetToDefaultFont": "기본값으로 재설정", - "textSize": "텍스트 크기", - "h1": "헤딩 1", - "h2": "헤딩 2", - "h3": "헤딩 3", - "alignLeft": "왼쪽 정렬", - "alignRight": "오른쪽 정렬", - "alignCenter": "가운데 정렬", - "link": "링크", - "textAlign": "텍스트 정렬", - "moreOptions": "더 많은 옵션", - "font": "글꼴", - "suggestions": "제안", - "turnInto": "변환" - }, - "errorBlock": { - "theBlockIsNotSupported": "블록 콘텐츠를 구문 분석할 수 없습니다", - "clickToCopyTheBlockContent": "블록 콘텐츠를 복사하려면 클릭", - "blockContentHasBeenCopied": "블록 콘텐츠가 복사되었습니다.", - "parseError": "{} 블록을 구문 분석하는 동안 오류가 발생했습니다.", - "copyBlockContent": "블록 콘텐츠 복사" - }, - "mobilePageSelector": { - "title": "페이지 선택", - "failedToLoad": "페이지 목록을 로드하지 못했습니다", - "noPagesFound": "페이지를 찾을 수 없습니다" - }, - "attachmentMenu": { - "choosePhoto": "사진 선택", - "takePicture": "사진 찍기", - "chooseFile": "파일 선택" } }, "board": { "column": { - "label": "열", - "createNewCard": "새로 만들기", - "renameGroupTooltip": "그룹 이름 변경", - "createNewColumn": "새 그룹 추가", - "addToColumnTopTooltip": "맨 위에 새 카드 추가", - "addToColumnBottomTooltip": "맨 아래에 새 카드 추가", - "renameColumn": "이름 변경", - "hideColumn": "숨기기", - "newGroup": "새 그룹", - "deleteColumn": "삭제", - "deleteColumnConfirmation": "이 그룹과 그룹 내 모든 카드를 삭제합니다. 계속하시겠습니까?" + "createNewCard": "추가" }, - "hiddenGroupSection": { - "sectionTitle": "숨겨진 그룹", - "collapseTooltip": "숨겨진 그룹 숨기기", - "expandTooltip": "숨겨진 그룹 보기" - }, - "cardDetail": "카드 세부 정보", - "cardActions": "카드 작업", - "cardDuplicated": "카드가 복제되었습니다", - "cardDeleted": "카드가 삭제되었습니다", - "showOnCard": "카드 세부 정보에 표시", - "setting": "설정", - "propertyName": "속성 이름", "menuName": "보드", - "showUngrouped": "그룹화되지 않은 항목 표시", - "ungroupedButtonText": "그룹화되지 않음", - "ungroupedButtonTooltip": "어떤 그룹에도 속하지 않는 카드가 포함되어 있습니다", - "ungroupedItemsTitle": "보드에 추가하려면 클릭", - "groupBy": "그룹 기준", - "groupCondition": "그룹 조건", - "referencedBoardPrefix": "보기", - "notesTooltip": "내부에 노트 있음", + "referencedBoardPrefix": "관점", "mobile": { - "editURL": "URL 편집", "showGroup": "그룹 표시", "showGroupContent": "이 그룹을 보드에 표시하시겠습니까?", - "failedToLoad": "보드 보기를 로드하지 못했습니다" - }, - "dateCondition": { - "weekOf": "{} - {} 주", - "today": "오늘", - "yesterday": "어제", - "tomorrow": "내일", - "lastSevenDays": "지난 7일", - "nextSevenDays": "다음 7일", - "lastThirtyDays": "지난 30일", - "nextThirtyDays": "다음 30일" - }, - "noGroup": "그룹화할 속성 없음", - "noGroupDesc": "보드 보기를 표시하려면 그룹화할 속성이 필요합니다", - "media": { - "cardText": "{} {}", - "fallbackName": "파일" + "failedToLoad": "보드 보기를 로드하지 못했습니다." } }, "calendar": { - "menuName": "캘린더", - "defaultNewCalendarTitle": "제목 없음", - "newEventButtonTooltip": "새 이벤트 추가", + "menuName": "달력", + "defaultNewCalendarTitle": "무제", "navigation": { "today": "오늘", "jumpToday": "오늘로 이동", - "previousMonth": "이전 달", - "nextMonth": "다음 달", - "views": { - "day": "일", - "week": "주", - "month": "월", - "year": "년" - } - }, - "mobileEventScreen": { - "emptyTitle": "이벤트 없음", - "emptyBody": "이 날에 이벤트를 생성하려면 더하기 버튼을 누르세요." + "previousMonth": "지난달", + "nextMonth": "다음 달" }, "settings": { "showWeekNumbers": "주 번호 표시", - "showWeekends": "주말 표시", - "firstDayOfWeek": "주 시작일", - "layoutDateField": "캘린더 레이아웃 기준", - "changeLayoutDateField": "레이아웃 필드 변경", + "showWeekends": "주말 보기", + "firstDayOfWeek": "주 시작", + "layoutDateField": "레이아웃 캘린더", "noDateTitle": "날짜 없음", - "noDateHint": { - "zero": "일정이 없는 이벤트가 여기에 표시됩니다", - "one": "{count}개의 일정이 없는 이벤트", - "other": "{count}개의 일정이 없는 이벤트" - }, - "unscheduledEventsTitle": "일정이 없는 이벤트", - "clickToAdd": "캘린더에 추가하려면 클릭", - "name": "캘린더 설정", - "clickToOpen": "레코드를 열려면 클릭" + "clickToAdd": "캘린더에 추가하려면 클릭하세요.", + "name": "달력 레이아웃", + "noDateHint": "예약되지 않은 일정이 여기에 표시됩니다." }, - "referencedCalendarPrefix": "보기", - "quickJumpYear": "이동", - "duplicateEvent": "이벤트 복제" + "referencedCalendarPrefix": "관점" }, "errorDialog": { "title": "@:appName 오류", - "howToFixFallback": "불편을 드려 죄송합니다! GitHub 페이지에 오류를 설명하는 문제를 제출하세요.", - "howToFixFallbackHint1": "불편을 드려 죄송합니다! ", - "howToFixFallbackHint2": " 페이지에 오류를 설명하는 문제를 제출하세요.", + "howToFixFallback": "불편을 끼쳐드려 죄송합니다! 오류를 설명하는 문제를 GitHub 페이지에 제출하세요.", "github": "GitHub에서 보기" }, "search": { "label": "검색", - "sidebarSearchIcon": "검색하고 페이지로 빠르게 이동", "placeholder": { - "actions": "작업 검색..." + "actions": "검색 작업..." } }, "message": { "copy": { - "success": "복사됨!", + "success": "복사했습니다!", "fail": "복사할 수 없음" } }, - "unSupportBlock": "현재 버전에서는 이 블록을 지원하지 않습니다.", + "unSupportBlock": "현재 버전은 이 블록을 지원하지 않습니다.", "views": { "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." }, - "colors": { - "custom": "사용자 정의", - "default": "기본", - "red": "빨간색", - "orange": "주황색", - "yellow": "노란색", - "green": "녹색", - "blue": "파란색", - "purple": "보라색", - "pink": "분홍색", - "brown": "갈색", - "gray": "회색" - }, - "emoji": { - "emojiTab": "이모지", - "search": "이모지 검색", - "noRecent": "최근 사용한 이모지 없음", - "noEmojiFound": "이모지를 찾을 수 없음", - "filter": "필터", - "random": "무작위", - "selectSkinTone": "피부 톤 선택", - "remove": "이모지 제거", - "categories": { - "smileys": "스마일리 및 감정", - "people": "사람", - "animals": "자연", - "food": "음식", - "activities": "활동", - "places": "장소", - "objects": "사물", - "symbols": "기호", - "flags": "깃발", - "nature": "자연", - "frequentlyUsed": "자주 사용됨" - }, - "skinTone": { - "default": "기본", - "light": "밝은", - "mediumLight": "중간 밝은", - "medium": "중간", - "mediumDark": "중간 어두운", - "dark": "어두운" - }, - "openSourceIconsFrom": "오픈 소스 아이콘 제공" - }, - "inlineActions": { - "noResults": "결과 없음", - "recentPages": "최근 페이지", - "pageReference": "페이지 참조", - "docReference": "문서 참조", - "boardReference": "보드 참조", - "calReference": "캘린더 참조", - "gridReference": "그리드 참조", - "date": "날짜", - "reminder": { - "groupTitle": "알림", - "shortKeyword": "알림" - }, - "createPage": "\"{}\" 하위 페이지 생성" - }, - "datePicker": { - "dateTimeFormatTooltip": "설정에서 날짜 및 시간 형식 변경", - "dateFormat": "날짜 형식", - "includeTime": "시간 포함", - "isRange": "종료 날짜", - "timeFormat": "시간 형식", - "clearDate": "날짜 지우기", - "reminderLabel": "알림", - "selectReminder": "알림 선택", - "reminderOptions": { - "none": "없음", - "atTimeOfEvent": "이벤트 시간", - "fiveMinsBefore": "5분 전", - "tenMinsBefore": "10분 전", - "fifteenMinsBefore": "15분 전", - "thirtyMinsBefore": "30분 전", - "oneHourBefore": "1시간 전", - "twoHoursBefore": "2시간 전", - "onDayOfEvent": "이벤트 당일", - "oneDayBefore": "1일 전", - "twoDaysBefore": "2일 전", - "oneWeekBefore": "1주일 전", - "custom": "사용자 정의" - } - }, - "relativeDates": { - "yesterday": "어제", - "today": "오늘", - "tomorrow": "내일", - "oneWeek": "1주일" - }, - "notificationHub": { - "title": "알림", - "mobile": { - "title": "업데이트" - }, - "emptyTitle": "모두 확인했습니다!", - "emptyBody": "대기 중인 알림이나 작업이 없습니다. 평온을 즐기세요.", - "tabs": { - "inbox": "받은 편지함", - "upcoming": "다가오는" - }, - "actions": { - "markAllRead": "모두 읽음으로 표시", - "showAll": "모두", - "showUnreads": "읽지 않음" - }, - "filters": { - "ascending": "오름차순", - "descending": "내림차순", - "groupByDate": "날짜별 그룹", - "showUnreadsOnly": "읽지 않은 항목만 표시", - "resetToDefault": "기본값으로 재설정" - } - }, - "reminderNotification": { - "title": "알림", - "message": "잊기 전에 확인하세요!", - "tooltipDelete": "삭제", - "tooltipMarkRead": "읽음으로 표시", - "tooltipMarkUnread": "읽지 않음으로 표시" - }, - "findAndReplace": { - "find": "찾기", - "previousMatch": "이전 일치 항목", - "nextMatch": "다음 일치 항목", - "close": "닫기", - "replace": "교체", - "replaceAll": "모두 교체", - "noResult": "결과 없음", - "caseSensitive": "대소문자 구분", - "searchMore": "더 많은 결과를 찾으려면 검색" - }, - "error": { - "weAreSorry": "죄송합니다", - "loadingViewError": "이 보기를 로드하는 데 문제가 있습니다. 인터넷 연결을 확인하고 앱을 새로 고침하세요. 문제가 계속되면 팀에 문의하세요.", - "syncError": "다른 장치에서 데이터가 동기화되지 않음", - "syncErrorHint": "마지막으로 편집한 장치에서 이 페이지를 다시 열고 현재 장치에서 다시 열어보세요.", - "clickToCopy": "오류 코드를 복사하려면 클릭" - }, - "editor": { - "bold": "굵게", - "bulletedList": "글머리 기호 목록", - "bulletedListShortForm": "글머리 기호", - "checkbox": "체크박스", - "embedCode": "코드 삽입", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "강조", - "color": "색상", - "image": "이미지", - "date": "날짜", - "page": "페이지", - "italic": "기울임꼴", - "link": "링크", - "numberedList": "번호 매기기 목록", - "numberedListShortForm": "번호 매기기", - "toggleHeading1ShortForm": "토글 h1", - "toggleHeading2ShortForm": "토글 h2", - "toggleHeading3ShortForm": "토글 h3", - "quote": "인용", - "strikethrough": "취소선", - "text": "텍스트", - "underline": "밑줄", - "fontColorDefault": "기본", - "fontColorGray": "회색", - "fontColorBrown": "갈색", - "fontColorOrange": "주황색", - "fontColorYellow": "노란색", - "fontColorGreen": "녹색", - "fontColorBlue": "파란색", - "fontColorPurple": "보라색", - "fontColorPink": "분홍색", - "fontColorRed": "빨간색", - "backgroundColorDefault": "기본 배경", - "backgroundColorGray": "회색 배경", - "backgroundColorBrown": "갈색 배경", - "backgroundColorOrange": "주황색 배경", - "backgroundColorYellow": "노란색 배경", - "backgroundColorGreen": "녹색 배경", - "backgroundColorBlue": "파란색 배경", - "backgroundColorPurple": "보라색 배경", - "backgroundColorPink": "분홍색 배경", - "backgroundColorRed": "빨간색 배경", - "backgroundColorLime": "라임색 배경", - "backgroundColorAqua": "청록색 배경", - "done": "완료", - "cancel": "취소", - "tint1": "색조 1", - "tint2": "색조 2", - "tint3": "색조 3", - "tint4": "색조 4", - "tint5": "색조 5", - "tint6": "색조 6", - "tint7": "색조 7", - "tint8": "색조 8", - "tint9": "색조 9", - "lightLightTint1": "보라색", - "lightLightTint2": "분홍색", - "lightLightTint3": "연분홍색", - "lightLightTint4": "주황색", - "lightLightTint5": "노란색", - "lightLightTint6": "라임색", - "lightLightTint7": "녹색", - "lightLightTint8": "청록색", - "lightLightTint9": "파란색", - "urlHint": "URL", - "mobileHeading1": "헤딩 1", - "mobileHeading2": "헤딩 2", - "mobileHeading3": "헤딩 3", - "mobileHeading4": "헤딩 4", - "mobileHeading5": "헤딩 5", - "mobileHeading6": "헤딩 6", - "textColor": "텍스트 색상", - "backgroundColor": "배경 색상", - "addYourLink": "링크 추가", - "openLink": "링크 열기", - "copyLink": "링크 복사", - "removeLink": "링크 제거", - "editLink": "링크 편집", - "linkText": "텍스트", - "linkTextHint": "텍스트를 입력하세요", - "linkAddressHint": "URL을 입력하세요", - "highlightColor": "강조 색상", - "clearHighlightColor": "강조 색상 지우기", - "customColor": "사용자 정의 색상", - "hexValue": "16진수 값", - "opacity": "불투명도", - "resetToDefaultColor": "기본 색상으로 재설정", - "ltr": "LTR", - "rtl": "RTL", - "auto": "자동", - "cut": "잘라내기", - "copy": "복사", - "paste": "붙여넣기", - "find": "찾기", - "select": "선택", - "selectAll": "모두 선택", - "previousMatch": "이전 일치 항목", - "nextMatch": "다음 일치 항목", - "closeFind": "닫기", - "replace": "교체", - "replaceAll": "모두 교체", - "regex": "정규식", - "caseSensitive": "대소문자 구분", - "uploadImage": "이미지 업로드", - "urlImage": "URL 이미지", - "incorrectLink": "잘못된 링크", - "upload": "업로드", - "chooseImage": "이미지 선택", - "loading": "로드 중", - "imageLoadFailed": "이미지 로드 실패", - "divider": "구분선", - "table": "테이블", - "colAddBefore": "앞에 추가", - "rowAddBefore": "앞에 추가", - "colAddAfter": "뒤에 추가", - "rowAddAfter": "뒤에 추가", - "colRemove": "제거", - "rowRemove": "제거", - "colDuplicate": "복제", - "rowDuplicate": "복제", - "colClear": "내용 지우기", - "rowClear": "내용 지우기", - "slashPlaceHolder": "'/'를 입력하여 블록을 삽입하거나 입력 시작", - "typeSomething": "무언가 입력...", - "toggleListShortForm": "토글", - "quoteListShortForm": "인용", - "mathEquationShortForm": "수식", - "codeBlockShortForm": "코드" - }, - "favorite": { - "noFavorite": "즐겨찾기 페이지 없음", - "noFavoriteHintText": "페이지를 왼쪽으로 스와이프하여 즐겨찾기에 추가하세요", - "removeFromSidebar": "사이드바에서 제거", - "addToSidebar": "사이드바에 고정" - }, - "cardDetails": { - "notesPlaceholder": "/를 입력하여 블록을 삽입하거나 입력 시작" - }, - "blockPlaceholders": { - "todoList": "할 일", - "bulletList": "목록", - "numberList": "목록", - "quote": "인용", - "heading": "헤딩 {}" - }, - "titleBar": { - "pageIcon": "페이지 아이콘", - "language": "언어", - "font": "글꼴", - "actions": "작업", - "date": "날짜", - "addField": "필드 추가", - "userIcon": "사용자 아이콘" - }, - "noLogFiles": "로그 파일이 없습니다", "newSettings": { "myAccount": { - "title": "내 계정", - "subtitle": "프로필을 사용자 정의하고, 계정 보안을 관리하고, AI 키를 열거나 계정에 로그인하세요.", - "profileLabel": "계정 이름 및 프로필 이미지", - "profileNamePlaceholder": "이름 입력", - "accountSecurity": "계정 보안", - "2FA": "2단계 인증", - "aiKeys": "AI 키", - "accountLogin": "계정 로그인", - "updateNameError": "이름 업데이트 실패", - "updateIconError": "아이콘 업데이트 실패", - "aboutAppFlowy": "@:appName 정보", "deleteAccount": { - "title": "계정 삭제", - "subtitle": "계정과 모든 데이터를 영구적으로 삭제합니다.", - "description": "계정을 영구적으로 삭제하고 모든 작업 공간에서 액세스를 제거합니다.", - "deleteMyAccount": "내 계정 삭제", - "dialogTitle": "계정 삭제", - "dialogContent1": "계정을 영구적으로 삭제하시겠습니까?", - "dialogContent2": "이 작업은 되돌릴 수 없으며, 모든 작업 공간에서 액세스를 제거하고, 개인 작업 공간을 포함한 전체 계정을 삭제하며, 모든 공유 작업 공간에서 제거됩니다.", - "confirmHint1": "\"@:newSettings.myAccount.deleteAccount.confirmHint3\"를 입력하여 확인하세요.", - "confirmHint2": "이 작업은 되돌릴 수 없으며, 계정과 모든 관련 데이터를 영구적으로 삭제합니다.", - "confirmHint3": "내 계정 삭제", - "checkToConfirmError": "삭제를 확인하려면 확인란을 선택해야 합니다", - "failedToGetCurrentUser": "현재 사용자 이메일을 가져오지 못했습니다", - "confirmTextValidationFailed": "확인 텍스트가 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"와 일치하지 않습니다", - "deleteAccountSuccess": "계정이 성공적으로 삭제되었습니다" + "dialogContent2": "이 작업은 실행 취소할 수 없으며 모든 작업 공간에서의 액세스가 제거되고, 개인 작업 공간을 포함한 전체 계정이 삭제되고, 모든 공유 작업 공간에서 제거됩니다." } - }, - "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": "예: 마케팅, 엔지니어링, 인사", - "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": "공간 아이콘" + "createNewSpace": "새로운 스페이스 생성", + "defaultSpaceName": "일반" }, "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": "그리드 보기만 게시할 수 있습니다", - "database": { - "zero": "선택한 {} 보기 게시", - "one": "선택한 {} 보기 게시", - "many": "선택한 {} 보기 게시", - "other": "선택한 {} 보기 게시" - }, - "mustSelectPrimaryDatabase": "기본 보기를 선택해야 합니다", - "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 최소 하나의 데이터베이스를 선택하세요.", - "unableToDeselectPrimaryDatabase": "기본 보기를 선택 해제할 수 없습니다", - "saveThisPage": "이 템플릿으로 시작", - "duplicateTitle": "추가할 위치 선택", - "selectWorkspace": "작업 공간 선택", - "addTo": "추가", - "duplicateSuccessfully": "작업 공간에 추가되었습니다", - "duplicateSuccessfullyDescription": "AppFlowy가 설치되어 있지 않습니까? '다운로드'를 클릭하면 다운로드가 자동으로 시작됩니다.", - "downloadIt": "다운로드", - "openApp": "앱에서 열기", - "duplicateFailed": "복제 실패", - "membersCount": { - "zero": "멤버 없음", - "one": "1명의 멤버", - "many": "{count}명의 멤버", - "other": "{count}명의 멤버" - }, - "useThisTemplate": "템플릿 사용" - }, - "web": { - "continue": "계속", - "or": "또는", - "continueWithGoogle": "Google로 계속", - "continueWithGithub": "GitHub로 계속", - "continueWithDiscord": "Discord로 계속", - "continueWithApple": "Apple로 계속", - "moreOptions": "더 많은 옵션", - "collapse": "접기", - "signInAgreement": "\"계속\"을 클릭하면 AppFlowy의", - "and": "및", - "termOfUse": "이용 약관", - "privacyPolicy": "개인정보 보호정책", - "signInError": "로그인 오류", - "login": "가입 또는 로그인", - "fileBlock": { - "uploadedAt": "{time}에 업로드됨", - "linkedAt": "{time}에 링크 추가됨", - "empty": "파일 업로드 또는 삽입", - "uploadFailed": "업로드 실패, 다시 시도하세요", - "retry": "다시 시도" - }, - "importNotion": "Notion에서 가져오기", - "import": "가져오기", - "importSuccess": "성공적으로 업로드되었습니다", - "importSuccessMessage": "가져오기가 완료되면 알림을 받게 됩니다. 이후 사이드바에서 가져온 페이지를 확인할 수 있습니다.", - "importFailed": "가져오기 실패, 파일 형식을 확인하세요", - "dropNotionFile": "Notion zip 파일을 여기에 드롭하여 업로드하거나 클릭하여 찾아보기", - "error": { - "pageNameIsEmpty": "페이지 이름이 비어 있습니다. 다른 이름을 시도하세요" - } - }, - "globalComment": { - "comments": "댓글", - "addComment": "댓글 추가", - "reactedBy": "반응한 사람", - "addReaction": "반응 추가", - "reactedByMore": "및 {count}명", - "showSeconds": { - "one": "1초 전", - "other": "{count}초 전", - "zero": "방금", - "many": "{count}초 전" - }, - "showMinutes": { - "one": "1분 전", - "other": "{count}분 전", - "many": "{count}분 전" - }, - "showHours": { - "one": "1시간 전", - "other": "{count}시간 전", - "many": "{count}시간 전" - }, - "showDays": { - "one": "1일 전", - "other": "{count}일 전", - "many": "{count}일 전" - }, - "showMonths": { - "one": "1개월 전", - "other": "{count}개월 전", - "many": "{count}개월 전" - }, - "showYears": { - "one": "1년 전", - "other": "{count}년 전", - "many": "{count}년 전" - }, - "reply": "답글", - "deleteComment": "댓글 삭제", - "youAreNotOwner": "이 댓글의 소유자가 아닙니다", - "confirmDeleteDescription": "이 댓글을 삭제하시겠습니까?", - "hasBeenDeleted": "삭제됨", - "replyingTo": "답글 대상", - "noAccessDeleteComment": "이 댓글을 삭제할 수 없습니다", - "collapse": "접기", - "readMore": "더 읽기", - "failedToAddComment": "댓글 추가 실패", - "commentAddedSuccessfully": "댓글이 성공적으로 추가되었습니다.", - "commentAddedSuccessTip": "댓글을 추가하거나 답글을 달았습니다. 최신 댓글을 보려면 상단으로 이동하시겠습니까?" + "saveThisPage": "이 템플릿으로 시작" }, "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": "추가 텍스트 없음" + "deleteFromTemplate": "템플릿 목록에서 제거", + "relatedTemplates": "관련된 템플릿 목록", + "deleteTemplate": "템플릿 제거", + "removeRelatedTemplate": "관련된 템플릿 제거", + "label": "템플릿 목록" }, "subscribe": { - "upgradePlanTitle": "플랜 비교 및 선택", - "yearly": "연간", - "save": "{discount}% 절약", - "monthly": "월별", - "priceIn": "가격 ", - "free": "무료", - "pro": "Pro", - "freeDescription": "모든 것을 정리하기 위한 최대 2명의 개인용", - "proDescription": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", - "proDuration": { - "monthly": "월별 청구되는 멤버당 월별", - "yearly": "연간 청구되는 멤버당 월별" - }, - "cancel": "다운그레이드", - "changePlan": "Pro 플랜으로 업그레이드", - "everythingInFree": "무료 플랜의 모든 기능 +", - "currentPlan": "현재", - "freeDuration": "영원히", - "freePoints": { - "first": "최대 2명의 협업 작업 공간", - "second": "무제한 페이지 및 블록", - "three": "5 GB 저장 공간", - "four": "지능형 검색", - "five": "20 AI 응답", - "six": "모바일 앱", - "seven": "실시간 협업" - }, - "proPoints": { - "first": "무제한 저장 공간", - "second": "최대 10명의 작업 공간 멤버", - "three": "무제한 AI 응답", - "four": "무제한 파일 업로드", - "five": "맞춤 네임스페이스" - }, "cancelPlan": { - "title": "떠나셔서 아쉽습니다", - "success": "구독이 성공적으로 취소되었습니다", - "description": "@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.", - "commonOther": "기타", - "otherHint": "여기에 답변을 작성하세요", - "questionOne": { - "question": "@:appName Pro 구독을 취소한 이유는 무엇입니까?", - "answerOne": "비용이 너무 높음", - "answerTwo": "기능이 기대에 미치지 못함", - "answerThree": "더 나은 대안을 찾음", - "answerFour": "비용을 정당화할 만큼 충분히 사용하지 않음", - "answerFive": "서비스 문제 또는 기술적 어려움" - }, - "questionTwo": { - "question": "미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?", - "answerOne": "매우 가능성이 높음", - "answerTwo": "어느 정도 가능성이 있음", - "answerThree": "잘 모르겠음", - "answerFour": "가능성이 낮음", - "answerFive": "매우 가능성이 낮음" - }, "questionThree": { - "question": "구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?", - "answerOne": "다중 사용자 협업", - "answerTwo": "더 긴 시간 버전 기록", - "answerThree": "무제한 AI 응답", - "answerFour": "로컬 AI 모델 액세스" - }, - "questionFour": { - "question": "@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?", - "answerOne": "훌륭함", - "answerTwo": "좋음", - "answerThree": "보통", - "answerFour": "평균 이하", - "answerFive": "불만족" + "answerFour": "로컬 AI 모델에 대한 액세스" } } - }, - "ai": { - "contentPolicyViolation": "민감한 콘텐츠로 인해 이미지 생성에 실패했습니다. 입력을 다시 작성하고 다시 시도하세요", - "textLimitReachedDescription": "작업 공간의 무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "imageLimitReachedDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "limitReachedAction": { - "textDescription": "작업 공간의 무료 AI 응답이 부족합니다. 더 많은 응답을 받으려면 ", - "imageDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. ", - "upgrade": "업그레이드", - "toThe": " ", - "proPlan": "Pro 플랜", - "orPurchaseAn": " 또는 ", - "aiAddon": "AI 애드온을 구매하세요" - }, - "editing": "편집 중", - "analyzing": "분석 중", - "continueWritingEmptyDocumentTitle": "계속 작성 오류", - "continueWritingEmptyDocumentDescription": "문서의 내용을 확장하는 데 문제가 있습니다. 간단한 소개를 작성하면 나머지는 우리가 처리할 수 있습니다!" - }, - "autoUpdate": { - "criticalUpdateTitle": "계속하려면 업데이트가 필요합니다", - "criticalUpdateDescription": "경험을 향상시키기 위해 개선 사항을 추가했습니다! 앱을 계속 사용하려면 {currentVersion}에서 {newVersion}으로 업데이트하세요.", - "criticalUpdateButton": "업데이트", - "bannerUpdateTitle": "새 버전 사용 가능!", - "bannerUpdateDescription": "최신 기능 및 수정 사항을 받으세요. 지금 설치하려면 \"업데이트\"를 클릭하세요", - "bannerUpdateButton": "업데이트", - "settingsUpdateTitle": "새 버전 ({newVersion}) 사용 가능!", - "settingsUpdateDescription": "현재 버전: {currentVersion} (공식 빌드) → {newVersion}", - "settingsUpdateButton": "업데이트", - "settingsUpdateWhatsNew": "새로운 기능" - }, - "lockPage": { - "lockPage": "잠금", - "reLockPage": "다시 잠금", - "lockTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다. 잠금 해제하려면 클릭하세요.", - "pageLockedToast": "페이지가 잠겼습니다. 누군가 잠금을 해제할 때까지 편집이 비활성화됩니다.", - "lockedOperationTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다." - }, - "suggestion": { - "accept": "수락", - "keep": "유지", - "discard": "버리기", - "close": "닫기", - "tryAgain": "다시 시도", - "rewrite": "다시 작성", - "insertBelow": "아래에 삽입" } } diff --git a/frontend/resources/translations/mr-IN.json b/frontend/resources/translations/mr-IN.json deleted file mode 100644 index f86a1e0081..0000000000 --- a/frontend/resources/translations/mr-IN.json +++ /dev/null @@ -1,3210 +0,0 @@ -{ - "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/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 9473d7e2f0..e3ca580354 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -164,14 +164,14 @@ "questionBubble": { "shortcuts": "Skróty", "whatsNew": "Co nowego?", + "help": "Pomoc & Wsparcie", "markdown": "Markdown", "debug": { "name": "Informacje Debugowania", "success": "Skopiowano informacje debugowania do schowka!", "fail": "Nie mozna skopiować informacji debugowania do schowka" }, - "feedback": "Feedback", - "help": "Pomoc & Wsparcie" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Usuń, zmień nazwę i więcej...", diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 864d225095..51b585f14b 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -225,14 +225,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", + "help": "Ajuda e Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Informação de depuração copiada para a área de transferência!", "fail": "Falha ao copiar a informação de depuração para a área de transferência" }, - "feedback": "Opinião", - "help": "Ajuda e Suporte" + "feedback": "Opinião" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index c9892bf9df..617e097f6b 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -128,14 +128,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", + "help": "Ajuda & Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Copiar informação de depuração para o clipboard!", "fail": "Falha em copiar a informação de depuração para o clipboard" }, - "feedback": "Opinião", - "help": "Ajuda & Suporte" + "feedback": "Opinião" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index c45010b8fc..b89b26f0c5 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -76,14 +76,9 @@ }, "workspace": { "chooseWorkspace": "Выберите рабочее пространство", - "defaultName": "Моё рабочее пространство", "create": "Создать рабочее пространство", - "new": "Новое рабочее пространство", - "importFromNotion": "Импортировать с Notion", - "learnMore": "Узнать больше", "reset": "Сбросить рабочее пространство", "renameWorkspace": "Переименовать рабочее пространство", - "workspaceNameCannotBeEmpty": "Название рабочего пространства не может быть пустым", "resetWorkspacePrompt": "Сброс рабочего пространства приведет к удалению всех страниц и данных внутри него. Вы уверены, что хотите сбросить рабочее пространство? В качестве альтернативы вы можете обратиться в службу поддержки для восстановления рабочего пространства", "hint": "рабочее пространство", "notFoundError": "Рабочее пространство не найдено.", @@ -127,9 +122,7 @@ "visitSite": "Посетить сайт", "exportAsTab": "Экспортировать как", "publishTab": "Опубликовать", - "shareTab": "Поделиться", - "publishOnAppFlowy": "Выложить на AppFlowy", - "shareTabTitle": "Пригласить к сотрудничеству" + "shareTab": "Поделиться" }, "moreAction": { "small": "маленький", @@ -151,11 +144,6 @@ "csv": "CSV", "database": "База данных" }, - "emojiIconPicker": { - "iconUploader": { - "change": "Изменить" - } - }, "disclosureAction": { "rename": "Переименовать", "delete": "Удалить", @@ -167,8 +155,7 @@ "addToFavorites": "Добавить в избранное", "copyLink": "Скопировать ссылку", "changeIcon": "Изменить иконку", - "collapseAllPages": "Свернуть все подстраницы", - "lockPage": "Заблокировать страницу" + "collapseAllPages": "Свернуть все подстраницы" }, "blankPageTitle": "Пустая страница", "newPageText": "Новая страница", @@ -183,39 +170,17 @@ "relatedQuestion": "Связано", "serverUnavailable": "Сервис временно недоступен. Пожалуйста, повторите попытку позже.", "aiServerUnavailable": "🌈 Ой-ой! 🌈. Единорог съел наш ответ. Пожалуйста, повторите попытку!", - "retry": "Повторить", "clickToRetry": "Нажмите, чтобы повторить попытку", "regenerateAnswer": "Повторно сгенерировать", "question1": "Как использовать канбан для управления задачами.", "question2": "Объясните метод GTD.", "question3": "Зачем использовать Rust.", "question4": "Рецепт из того, что есть у меня на кухне.", - "aiMistakePrompt": "ИИ может ошибаться. Проверяйте важную информацию.", - "inputActionNoPages": "Нет результатов на странице", - "currentPage": "Текущая страница", - "regenerate": "Попробуйте ещё раз", - "addToNewPage": "Создать новую страницу", - "openPagePreviewFailedToast": "Не удалось открыть страницу", - "changeFormat": { - "actionButton": "Изменить формат", - "textOnly": "Текст", - "imageOnly": "Только изображение", - "textAndImage": "Текст и изображение", - "text": "Параграф", - "bullet": "Список маркеров", - "number": "Нумерованный список", - "defaultDescription": "Автоматический режим" - }, - "selectBanner": { - "selectMessages": "Выбрать сообщения", - "allSelected": "Все выбрано" - }, - "stopTooltip": "Остановить генерацию" + "aiMistakePrompt": "ИИ может ошибаться. Проверяйте важную информацию." }, "trash": { "text": "Корзина", "restoreAll": "Восстановить всё", - "restore": "Восстановить", "deleteAll": "Удалить всё", "pageHeader": { "fileName": "Имя файла", @@ -230,9 +195,6 @@ "title": "Вы уверены, что хотите восстановить все страницы в корзине?", "caption": "Это действие не может быть отменено." }, - "restorePage": { - "caption": "Вы уверены, что хотите восстановить эту страницу?" - }, "mobile": { "actions": "Действия с корзиной", "empty": "Корзина пуста", @@ -251,14 +213,14 @@ "questionBubble": { "shortcuts": "Горячие клавиши", "whatsNew": "Что нового?", + "help": "Помощь и поддержка", "markdown": "Markdown", "debug": { "name": "Отладочная информация", "success": "Отладочная информация скопирована в буфер обмена!", "fail": "Не удалось скопировать отладочную информацию в буфер обмена" }, - "feedback": "Обратная связь", - "help": "Помощь и поддержка" + "feedback": "Обратная связь" }, "menuAppHeader": { "moreButtonToolTip": "Удалить, переименовать и другие действия...", @@ -327,10 +289,7 @@ "removeSuccess": "Удалено успешно", "favoriteSpace": "Избранное", "RecentSpace": "Недавнее", - "Spaces": "Пространства", - "upgradeToPro": "Обновление до Pro", - "upgradeToAIMax": "Разблокируйте неограниченный ИИ", - "purchaseAIResponse": "Покупка " + "Spaces": "Пространства" }, "notifications": { "export": { @@ -364,7 +323,6 @@ "upload": "Загрузить", "edit": "Редактировать", "delete": "Удалить", - "copy": "Копировать", "duplicate": "Дублировать", "putback": "Вернуть", "update": "Обновить", @@ -376,7 +334,6 @@ "helpCenter": "Центр помощи", "add": "Добавить", "yes": "Да", - "no": "Нет", "clear": "Очистить", "remove": "Удалить", "dontRemove": "Не удалять", @@ -392,16 +349,6 @@ "more": "Больше", "create": "Создать", "close": "Закрыть", - "next": "Следующий", - "previous": "Предыдущий", - "submit": "Представить", - "download": "Скачать", - "backToHome": "Вернуться на главную", - "viewing": "Просмотр", - "editing": "Редактирование", - "gotIt": "Понятно", - "retry": "Повторить попытку", - "uploadFailed": "Загрузка не удалась.", "tryAGain": "Попробовать ещё раз", "Done": "Готово", "Cancel": "Отмена", @@ -429,28 +376,6 @@ }, "settings": { "title": "Настройки", - "popupMenuItem": { - "settings": "Настройки", - "members": "Участники", - "helpAndSupport": "Помощь и поддержка" - }, - "sites": { - "namespaceTitle": "Пространство имен", - "namespaceDescription": "Управляйте своим пространством имен и домашней страницей", - "namespaceHeader": "Пространство имен", - "homepageHeader": "Домашняя страница", - "updateNamespace": "Обновить пространство имен", - "removeHomepage": "Удалить домашнюю страницу", - "selectHomePage": "Выберите страницу", - "clearHomePage": "Очистить домашнюю страницу для этого пространства имен", - "customUrl": "Пользовательский URL-адрес", - "namespace": { - "description": "Это изменение будет применено ко всем опубликованным страницам, размещенным в этом пространстве имен." - }, - "publishedPage": { - "page": "Страница" - } - }, "accountPage": { "menuLabel": "Мой аккаунт", "title": "Мой аккаунт", @@ -1420,7 +1345,8 @@ "url": { "launch": "Открыть в браузере", "copy": "Скопировать URL", - "textFieldHint": "Введите URL-адрес" + "textFieldHint": "Введите URL-адрес", + "copiedNotification": "Скопировано в буфер обмена!" }, "relation": { "relatedDatabasePlaceLabel": "Связанная база данных", @@ -2257,15 +2183,5 @@ "privacyPolicy": "Политика конфиденциальности", "signInError": "Ошибка входа", "login": "Зарегистрироваться или войти" - }, - "ai": { - "limitReachedAction": { - "upgrade": "улучшить", - "proPlan": "план Pro", - "aiAddon": "Дополнение ИИ" - }, - "editing": "Редактирование", - "analyzing": "Анализ", - "more": "Более" } } diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 3210aa1f15..42855011b2 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -107,14 +107,14 @@ "questionBubble": { "shortcuts": "Genvägar", "whatsNew": "Vad nytt?", + "help": "Hjälp & Support", "markdown": "Prissänkning", "debug": { "name": "Felsökningsinfo", "success": "Kopierade felsökningsinfo till urklipp!", "fail": "Kunde inte kopiera felsökningsinfo till urklipp" }, - "feedback": "Återkoppling", - "help": "Hjälp & Support" + "feedback": "Återkoppling" }, "menuAppHeader": { "addPageTooltip": "Lägg till en underliggande sida", diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 78e5462d7f..0e888fdb9b 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -250,14 +250,14 @@ "questionBubble": { "shortcuts": "ทางลัด", "whatsNew": "มีอะไรใหม่?", + "help": "ช่วยเหลือและสนับสนุน", "markdown": "Markdown", "debug": { "name": "ข้อมูลดีบัก", "success": "คัดลอกข้อมูลดีบักไปยังคลิปบอร์ดแล้ว!", "fail": "ไม่สามารถคัดลอกข้อมูลดีบักไปยังคลิปบอร์ด" }, - "feedback": "ข้อเสนอแนะ", - "help": "ช่วยเหลือและสนับสนุน" + "feedback": "ข้อเสนอแนะ" }, "menuAppHeader": { "moreButtonToolTip": "ลบ เปลี่ยนชื่อ และอื่นๆ...", @@ -1570,7 +1570,8 @@ "url": { "launch": "เปิดในเบราว์เซอร์", "copy": "คัดลอก URL", - "textFieldHint": "ป้อน URL" + "textFieldHint": "ป้อน URL", + "copiedNotification": "คัดลอกไปยังคลิปบอร์ดแล้ว!" }, "relation": { "relatedDatabasePlaceLabel": "ฐานข้อมูลที่เกี่ยวข้อง", diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 0eeac684c6..d23566e512 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -288,14 +288,14 @@ "questionBubble": { "shortcuts": "Kısayollar", "whatsNew": "Yenilikler", + "help": "Yardım ve Destek", "markdown": "Markdown", "debug": { "name": "Hata Ayıklama Bilgisi", "success": "Hata ayıklama bilgisi panoya kopyalandı!", "fail": "Hata ayıklama bilgisi panoya kopyalanamadı" }, - "feedback": "Geri Bildirim", - "help": "Yardım ve Destek" + "feedback": "Geri Bildirim" }, "menuAppHeader": { "moreButtonToolTip": "Kaldır, yeniden adlandır ve daha fazlası...", @@ -1608,7 +1608,8 @@ "url": { "launch": "Bağlantıyı tarayıcıda aç", "copy": "Bağlantıyı panoya kopyala", - "textFieldHint": "Bir URL girin" + "textFieldHint": "Bir URL girin", + "copiedNotification": "Panoya kopyalandı!" }, "relation": { "relatedDatabasePlaceLabel": "İlişkili Veritabanı", @@ -2517,12 +2518,12 @@ "dialogTitle": "Hesabı sil", "dialogContent1": "Hesabınızı kalıcı olarak silmek istediğinizden emin misiniz?", "dialogContent2": "Bu işlem GERİ ALINAMAZ ve tüm çalışma alanlarından erişiminizi kaldıracak, özel çalışma alanları dahil tüm hesabınızı silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır.", - "confirmHint1": "Onaylamak için lütfen \"@:newSettings.myAccount.deleteAccount.confirmHint3\" yazın.", + "confirmHint1": "Onaylamak için lütfen \"HESABIMI SİL\" yazın.", "confirmHint2": "Bu işlemin GERİ ALINAMAZ olduğunu ve hesabımı ve ilişkili tüm verileri kalıcı olarak sileceğini anlıyorum.", "confirmHint3": "HESABIMI SİL", "checkToConfirmError": "Silme işlemini onaylamak için kutuyu işaretlemelisiniz", "failedToGetCurrentUser": "Mevcut kullanıcı e-postası alınamadı", - "confirmTextValidationFailed": "Onay metniniz \"@:newSettings.myAccount.deleteAccount.confirmHint3\" ile eşleşmiyor", + "confirmTextValidationFailed": "Onay metniniz \"HESABIMI SİL\" ile eşleşmiyor", "deleteAccountSuccess": "Hesap başarıyla silindi" } }, diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 394801ed21..45cc93fd43 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -64,7 +64,7 @@ "settings": "Налаштування", "magicLinkSent": "Magic Link надіслано!", "invalidEmail": "Будь ласка, введіть дійсну адресу електронної пошти", - "alreadyHaveAnAccount": "Вже є акаунт?", + "alreadyHaveAnAccount": "Вже є аккаунт?", "logIn": "Авторизуватися", "generalError": "Щось пішло не так. Будь ласка спробуйте пізніше", "limitRateError": "З міркувань безпеки ви можете запитувати чарівне посилання лише кожні 60 секунд", @@ -225,14 +225,14 @@ "questionBubble": { "shortcuts": "Комбінації клавіш", "whatsNew": "Що нового?", + "help": "Довідка та підтримка", "markdown": "Markdown", "debug": { "name": "Інформація для налагодження", "success": "Інформацію для налагодження скопійовано в буфер обміну!", "fail": "Не вдалося скопіювати інформацію для налагодження в буфер обміну" }, - "feedback": "Зворотний зв'язок", - "help": "Довідка та підтримка" + "feedback": "Зворотний зв'язок" }, "menuAppHeader": { "moreButtonToolTip": "Видалити, перейменувати та інше...", @@ -362,7 +362,7 @@ "helpCenter": "Центр допомоги", "add": "Додати", "yes": "Так", - "no": "Ні", + "no": "Немає", "clear": "Очистити", "remove": "Видалити", "dontRemove": "Не видаляйте", @@ -810,7 +810,7 @@ "description": "Ви впевнені, що хочете видалити {plan}? Ви негайно втратите доступ до функцій і переваг {plan}." } }, - "currentPeriodBadge": "ПОТОЧНИЙ", + "currentPeriodBadge": "ПОТОМ", "changePeriod": "Період зміни", "planPeriod": "{} період", "monthlyInterval": "Щомісяця", @@ -1445,7 +1445,8 @@ "url": { "launch": "Відкрити посилання в браузері", "copy": "Копіювати посилання в буфер обміну", - "textFieldHint": "Введіть URL" + "textFieldHint": "Введіть URL", + "copiedNotification": "Скопійовано в буфер обміну!" }, "relation": { "relatedDatabasePlaceLabel": "Пов'язана база даних", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index e60648590d..a506a84db3 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -226,14 +226,14 @@ "questionBubble": { "shortcuts": "Phím tắt", "whatsNew": "Có gì mới?", + "help": "Trợ giúp & Hỗ trợ", "markdown": "Markdown", "debug": { "name": "Thông tin gỡ lỗi", "success": "Đã sao chép thông tin gỡ lỗi vào khay nhớ tạm!", "fail": "Không thể sao chép thông tin gỡ lỗi vào khay nhớ tạm" }, - "feedback": "Nhận xét", - "help": "Trợ giúp & Hỗ trợ" + "feedback": "Nhận xét" }, "menuAppHeader": { "moreButtonToolTip": "Xóa, đổi tên và hơn thế nữa...", @@ -1439,7 +1439,8 @@ "url": { "launch": "Mở liên kết trong trình duyệt", "copy": "Sao chép URL", - "textFieldHint": "Nhập một URL" + "textFieldHint": "Nhập một URL", + "copiedNotification": "Đã sao chép vào bảng tạm!" }, "relation": { "relatedDatabasePlaceLabel": "Cơ sở dữ liệu liên quan", @@ -2269,12 +2270,12 @@ "dialogTitle": "Xóa tài khoản", "dialogContent1": "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không?", "dialogContent2": "Không thể hoàn tác hành động này và sẽ xóa quyền truy cập khỏi mọi không gian làm việc nhóm, xóa toàn bộ tài khoản của bạn, bao gồm cả không gian làm việc riêng tư và xóa bạn khỏi mọi không gian làm việc được chia sẻ.", - "confirmHint1": "Vui lòng nhập \"@:newSettings.myAccount.deleteAccount.confirmHint3\" để xác nhận.", + "confirmHint1": "Vui lòng nhập \"XÓA TÀI KHOẢN CỦA TÔI\" để xác nhận.", "confirmHint2": "Tôi hiểu rằng hành động này là không thể đảo ngược và sẽ xóa vĩnh viễn tài khoản của tôi cùng mọi dữ liệu liên quan.", "confirmHint3": "XÓA TÀI KHOẢN CỦA TÔI", "checkToConfirmError": "Bạn phải đánh dấu vào ô để xác nhận việc xóa", "failedToGetCurrentUser": "Không lấy được email người dùng hiện tại", - "confirmTextValidationFailed": "Văn bản xác nhận của bạn không khớp với \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", + "confirmTextValidationFailed": "Văn bản xác nhận của bạn không khớp với \"XÓA TÀI KHOẢN CỦA TÔI\"", "deleteAccountSuccess": "Tài khoản đã được xóa thành công" } }, diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index d1433929bc..ce0c760f3d 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -270,14 +270,14 @@ "questionBubble": { "shortcuts": "快捷键", "whatsNew": "新功能", + "help": "帮助和支持", "markdown": "Markdown", "debug": { "name": "调试信息", "success": "将调试信息复制到剪贴板!", "fail": "无法将调试信息复制到剪贴板" }, - "feedback": "反馈", - "help": "帮助和支持" + "feedback": "反馈" }, "menuAppHeader": { "moreButtonToolTip": "删除、重命名等等...", @@ -1270,7 +1270,8 @@ "url": { "launch": "在浏览器中打开链接", "copy": "将链接复制到剪贴板", - "textFieldHint": "输入 URL" + "textFieldHint": "输入 URL", + "copiedNotification": "已复制到剪贴板!" }, "relation": { "rowSearchTextFieldPlaceholder": "搜索" @@ -1326,62 +1327,6 @@ }, "document": { "selectADocumentToLinkTo": "选择要链接到的文档" - }, - "name": { - "textStyle": "文本样式", - "list": "列表", - "toggle": "切换", - "fileAndMedia": "文件与媒体", - "simpleTable": "简单表格", - "visuals": "视觉元素", - "document": "文档", - "advanced": "高级", - "text": "文本", - "heading1": "一级标题", - "heading2": "二级标题", - "heading3": "三级标题", - "image": "图片", - "bulletedList": "项目符号列表", - "numberedList": "编号列表", - "todoList": "待办事项列表", - "doc": "文档", - "linkedDoc": "链接到页面", - "grid": "网格", - "linkedGrid": "链接网格", - "kanban": "看板", - "linkedKanban": "链接看板", - "calendar": "日历", - "linkedCalendar": "链接日历", - "quote": "引用", - "divider": "分隔符", - "table": "表格", - "callout": "提示框", - "outline": "大纲", - "mathEquation": "数学公式", - "code": "代码", - "toggleList": "切换列表", - "toggleHeading1": "切换标题1", - "toggleHeading2": "切换标题2", - "toggleHeading3": "切换标题3", - "emoji": "表情符号", - "aiWriter": "向AI提问", - "dateOrReminder": "日期或提醒", - "photoGallery": "图片库", - "file": "文件", - "twoColumns": "两列", - "threeColumns": "三列", - "fourColumns": "四列" - }, - "subPage": { - "name": "文档", - "keyword1": "子页面", - "keyword2": "页面", - "keyword3": "子页面", - "keyword4": "插入页面", - "keyword5": "嵌入页面", - "keyword6": "新页面", - "keyword7": "创建页面", - "keyword8": "文档" } }, "selectionMenu": { @@ -1393,16 +1338,6 @@ "referencedGrid": "引用的网格", "referencedCalendar": "引用的日历", "referencedDocument": "参考文档", - "aiWriter": { - "userQuestion": "向AI提问", - "continueWriting": "继续写作", - "fixSpelling": "修正拼写和语法", - "improveWriting": "优化写作", - "summarize": "总结", - "explain": "解释", - "makeShorter": "缩短", - "makeLonger": "扩展" - }, "autoGeneratorMenuItemName": "AI 创作", "autoGeneratorTitleName": "AI: 让 AI 写些什么...", "autoGeneratorLearnMore": "学习更多", @@ -1463,7 +1398,7 @@ }, "optionAction": { "click": "点击", - "toOpenMenu": "打开菜单", + "toOpenMenu": " 来打开菜单", "delete": "删除", "duplicate": "复制", "turnInto": "变成", @@ -1473,10 +1408,8 @@ "align": "对齐", "left": "左", "center": "中心", - "right": "右", - "defaultColor": "默认", - "depth": "深度", - "copyLinkToBlock": "粘贴块链接" + "right": "又", + "defaultColor": "默认" }, "image": { "addAnImage": "添加图像", @@ -1517,27 +1450,6 @@ "placeholder": "粘贴视频链接", "copiedToPasteBoard": "视频链接已复制到剪贴板", "insertVideo": "添加视频" - }, - "linkPreview": { - "typeSelection": { - "pasteAs": "粘贴为", - "mention": "提及", - "URL": "URL", - "bookmark": "书签", - "embed": "嵌入" - }, - "linkPreviewMenu": { - "toMetion": "转换为提及", - "toUrl": "转换为URL", - "toEmbed": "转换为嵌入", - "toBookmark": "转换为书签", - "copyLink": "复制链接", - "replace": "替换", - "reload": "重新加载", - "removeLink": "移除链接", - "pasteHint": "粘贴 https://...", - "unableToDisplay": "无法显示" - } } }, "outlineBlock": { @@ -2008,14 +1920,7 @@ "deleteMyAccount": "删除我的账户", "dialogTitle": "删除帐户", "dialogContent1": "你确定要永久删除您的帐户吗?", - "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。", - "confirmHint1": "请输入 \"@:newSettings.myAccount.deleteAccount.confirmHint3\" 以确认。", - "confirmHint2": "我理解此操作是不可逆的,并且将永久删除我的帐户和所有关联数据。", - "confirmHint3": "删除我的账户", - "checkToConfirmError": "你必须勾选以确认删除。", - "failedToGetCurrentUser": "获取当前用户邮箱失败", - "confirmTextValidationFailed": "你的确认文本不匹配 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", - "deleteAccountSuccess": "账户删除成功" + "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。" } }, "workplace": { @@ -2092,4 +1997,4 @@ "yesterday": "昨天", "today": "今天" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index b5f4ff3d5f..fe66a58aa8 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -216,14 +216,14 @@ "questionBubble": { "shortcuts": "快捷鍵", "whatsNew": "有什麼新功能?", + "help": "幫助 & 支援", "markdown": "Markdown", "debug": { "name": "除錯資訊", "success": "已將除錯資訊複製到剪貼簿!", "fail": "無法將除錯資訊複製到剪貼簿" }, - "feedback": "意見回饋", - "help": "幫助 & 支援" + "feedback": "意見回饋" }, "menuAppHeader": { "moreButtonToolTip": "移除、重新命名等等...", @@ -838,7 +838,8 @@ "url": { "launch": "在瀏覽器中開啟", "copy": "複製網址", - "textFieldHint": "輸入網址" + "textFieldHint": "輸入網址", + "copiedNotification": "已複製到剪貼簿" }, "menuName": "網格", "referencedGridPrefix": "檢視", diff --git a/frontend/rust-lib/.vscode/launch.json b/frontend/rust-lib/.vscode/launch.json deleted file mode 100644 index 3b1e6e62a7..0000000000 --- a/frontend/rust-lib/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "AF-desktop: Debug Rust", - "type": "lldb", - // "request": "attach", - // "pid": "${command:pickMyProcess}" - // To launch the application directly, use the following configuration: - "request": "launch", - "program": "/Users/lucas.xu/Desktop/appflowy_backup/frontend/appflowy_flutter/build/macos/Build/Products/Debug/AppFlowy.app", - }, - ] - } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 51a3f1a3b2..c447cc03bb 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -14,284 +14,6 @@ dependencies = [ "syn 2.0.94", ] -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.4.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129d4c88e98860e1758c5de288d1632b07970a16d59bdf7b8d66053d582bb71f" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "ahash 0.8.6", - "base64 0.21.5", - "bitflags 2.4.0", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2 0.3.21", - "http 0.2.9", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd 0.13.2", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.9", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio 1.0.3", - "socket2 0.5.5", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots 0.22.6", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43428f3bf11dee6d166b00ec2df4e3aa8cc1606aaa0b7433c146852e2f4e03b" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "ahash 0.8.6", - "bytes", - "bytestring", - "cfg-if", - "cookie 0.16.2", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.5", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-web-lab" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6" -dependencies = [ - "actix-http", - "actix-router", - "actix-service", - "actix-utils", - "actix-web", - "actix-web-lab-derive", - "ahash 0.8.6", - "arc-swap", - "async-trait", - "bytes", - "bytestring", - "csv", - "derive_more", - "futures-core", - "futures-util", - "http 0.2.9", - "impl-more", - "itertools 0.12.1", - "local-channel", - "mediatype", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_html_form", - "serde_json", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "actix-web-lab-derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-ws" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535aec173810be3ca6f25dd5b4d431ae7125d62000aa3cbae1ec739921b02cf3" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "futures-core", - "tokio", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -342,58 +64,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "af-local-ai" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "af-plugin", - "anyhow", - "bytes", - "reqwest 0.11.27", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "af-mcp" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "anyhow", - "futures-util", - "mcp_daemon", - "serde", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "af-plugin" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "anyhow", - "cfg-if", - "crossbeam-utils", - "log", - "once_cell", - "parking_lot 0.12.1", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tracing", - "winreg 0.55.0", - "xattr", -] - [[package]] name = "again" version = "0.1.2" @@ -486,19 +156,19 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bincode", "getrandom 0.2.10", - "reqwest 0.12.15", + "reqwest 0.12.9", "serde", "serde_json", "serde_repr", @@ -513,16 +183,55 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bytes", "futures", + "pin-project", "serde", "serde_json", "serde_repr", "thiserror 1.0.64", - "uuid", +] + +[[package]] +name = "appflowy-local-ai" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13" +dependencies = [ + "anyhow", + "appflowy-plugin", + "bytes", + "reqwest 0.11.27", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "zip 2.2.0", + "zip-extensions", +] + +[[package]] +name = "appflowy-plugin" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13" +dependencies = [ + "anyhow", + "cfg-if", + "crossbeam-utils", + "log", + "once_cell", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror 1.0.64", + "tokio", + "tokio-stream", + "tracing", + "xattr", ] [[package]] @@ -574,15 +283,6 @@ dependencies = [ "zstd-safe 7.2.0", ] -[[package]] -name = "async-convert" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" -dependencies = [ - "async-trait", -] - [[package]] name = "async-lock" version = "3.4.0" @@ -594,31 +294,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-openai" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc0b1877fb1bc415caa14d1899f0f477e8eb38f2fe16f54be196d7c4a92e15c" -dependencies = [ - "async-convert", - "backoff", - "base64 0.21.5", - "bytes", - "derive_builder 0.12.0", - "futures", - "rand 0.8.5", - "reqwest 0.11.27", - "reqwest-eventsource", - "secrecy", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -632,9 +307,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -643,9 +318,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", @@ -726,7 +401,7 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", ] @@ -748,20 +423,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "futures-core", - "getrandom 0.2.10", - "instant", - "pin-project-lite", - "rand 0.8.5", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.69" @@ -887,31 +548,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bon" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c5f8abc69af414cbd6f2103bb668b91e584072f2105e4b38bed79b6ad0975f" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69edf39b6f321cb2699a93fc20c256adb839719c42676d03f7aa975e4e5581d" -dependencies = [ - "darling 0.20.11", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.94", -] - [[package]] name = "borsh" version = "1.5.1" @@ -1010,15 +646,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - [[package]] name = "bzip2" version = "0.4.4" @@ -1159,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "again", "anyhow", @@ -1190,7 +817,7 @@ dependencies = [ "pin-project", "prost 0.13.3", "rayon", - "reqwest 0.12.15", + "reqwest 0.12.9", "scraper 0.17.1", "semver", "serde", @@ -1201,7 +828,7 @@ dependencies = [ "tokio", "tokio-retry", "tokio-stream", - "tokio-tungstenite 0.20.1", + "tokio-tungstenite", "tokio-util", "tracing", "url", @@ -1214,7 +841,7 @@ dependencies = [ [[package]] name = "client-api-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "collab-entity", "collab-rt-entity", @@ -1227,7 +854,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "futures-channel", "futures-util", @@ -1236,19 +863,18 @@ dependencies = [ "percent-encoding", "thiserror 1.0.64", "tokio", - "tokio-tungstenite 0.20.1", + "tokio-tungstenite", "wasm-bindgen", "web-sys", ] [[package]] name = "cmd_lib" -version = "1.9.5" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "371c15a3c178d0117091bd84414545309ca979555b1aad573ef591ad58818d41" +checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62" dependencies = [ "cmd_lib_macros", - "env_logger 0.10.2", "faccess", "lazy_static", "log", @@ -1257,20 +883,20 @@ dependencies = [ [[package]] name = "cmd_lib_macros" -version = "1.9.5" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb844bd05be34d91eb67101329aeba9d3337094c04fd8507d821db7ebb488eaf" +checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0" dependencies = [ - "proc-macro-error2", + "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.94", + "syn 1.0.109", ] [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -1295,7 +921,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-trait", @@ -1335,7 +961,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -1356,7 +982,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "bytes", @@ -1376,7 +1002,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "arc-swap", @@ -1398,7 +1024,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-recursion", @@ -1440,6 +1066,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arc-swap", + "async-trait", "collab", "collab-database", "collab-document", @@ -1450,18 +1077,18 @@ dependencies = [ "diesel", "flowy-error", "flowy-sqlite", + "futures", "lib-infra", "serde", "serde_json", "tokio", "tracing", - "uuid", ] [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "async-stream", @@ -1499,7 +1126,7 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bincode", @@ -1514,14 +1141,14 @@ dependencies = [ "protoc-bin-vendored", "serde", "serde_repr", - "tokio-tungstenite 0.20.1", + "tokio-tungstenite", "yrs", ] [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "async-trait", @@ -1532,14 +1159,13 @@ dependencies = [ "thiserror 1.0.64", "tokio", "tracing", - "uuid", "yrs", ] [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242" dependencies = [ "anyhow", "collab", @@ -1625,23 +1251,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "cookie" version = "0.18.1" @@ -1659,7 +1268,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ - "cookie 0.18.1", + "cookie", "document-features", "idna 1.0.3", "log", @@ -1832,70 +1441,35 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.4" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.14.4" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", "syn 2.0.94", ] [[package]] name = "darling_macro" -version = "0.14.4" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", + "darling_core", "quote", "syn 2.0.94", ] @@ -1969,7 +1543,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "bincode", "bytes", @@ -2046,78 +1620,14 @@ dependencies = [ "syn 2.0.94", ] -[[package]] -name = "derive_builder" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" -dependencies = [ - "derive_builder_macro 0.12.0", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro 0.20.2", -] - -[[package]] -name = "derive_builder_core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "derive_builder_macro" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" -dependencies = [ - "derive_builder_core 0.12.0", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core 0.20.2", - "syn 2.0.94", -] - [[package]] name = "derive_more" version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version", "syn 1.0.109", ] @@ -2218,9 +1728,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "downcast-rs" -version = "2.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dtoa" @@ -2280,19 +1790,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -2313,6 +1810,7 @@ dependencies = [ name = "event-integration-test" version = "0.1.0" dependencies = [ + "anyhow", "assert-json-diff", "bytes", "chrono", @@ -2321,11 +1819,15 @@ dependencies = [ "collab-document", "collab-entity", "collab-folder", + "collab-plugins", + "dotenv", "flowy-ai", "flowy-ai-pub", "flowy-core", + "flowy-database-pub", "flowy-database2", "flowy-document", + "flowy-document-pub", "flowy-folder", "flowy-folder-pub", "flowy-notification", @@ -2337,6 +1839,7 @@ dependencies = [ "flowy-user", "flowy-user-pub", "futures", + "futures-util", "lib-dispatch", "lib-infra", "nanoid", @@ -2346,7 +1849,10 @@ dependencies = [ "serde", "serde_json", "strum", + "tempdir", + "thread-id", "tokio", + "tokio-postgres", "tracing", "uuid", "walkdir", @@ -2374,17 +1880,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "eventsource-stream" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" -dependencies = [ - "futures-core", - "nom", - "pin-project-lite", -] - [[package]] name = "faccess" version = "0.2.4" @@ -2406,6 +1901,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fancy-regex" version = "0.10.0" @@ -2476,6 +1977,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2496,11 +2003,10 @@ dependencies = [ name = "flowy-ai" version = "0.1.0" dependencies = [ - "af-local-ai", - "af-mcp", - "af-plugin", "allo-isolate", "anyhow", + "appflowy-local-ai", + "appflowy-plugin", "arc-swap", "base64 0.21.5", "bytes", @@ -2519,6 +2025,7 @@ dependencies = [ "lib-dispatch", "lib-infra", "log", + "md5", "notify", "pin-project", "protobuf", @@ -2535,20 +2042,20 @@ dependencies = [ "tracing-subscriber", "uuid", "validator 0.18.1", + "zip 2.2.0", + "zip-extensions", ] [[package]] name = "flowy-ai-pub" version = "0.1.0" dependencies = [ + "bytes", "client-api", "flowy-error", - "flowy-sqlite", "futures", "lib-infra", - "serde", "serde_json", - "uuid", ] [[package]] @@ -2588,9 +2095,8 @@ dependencies = [ name = "flowy-core" version = "0.1.0" dependencies = [ - "af-local-ai", - "af-plugin", "anyhow", + "appflowy-local-ai", "arc-swap", "base64 0.21.5", "bytes", @@ -2622,6 +2128,7 @@ dependencies = [ "flowy-storage-pub", "flowy-user", "flowy-user-pub", + "futures", "futures-core", "lib-dispatch", "lib-infra", @@ -2634,20 +2141,20 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "url", "uuid", + "walkdir", ] [[package]] name = "flowy-database-pub" version = "0.1.0" dependencies = [ + "anyhow", "client-api", "collab", "collab-entity", "flowy-error", "lib-infra", - "uuid", ] [[package]] @@ -2696,7 +2203,6 @@ dependencies = [ "tokio-util", "tracing", "url", - "uuid", "validator 0.18.1", ] @@ -2774,11 +2280,11 @@ dependencies = [ name = "flowy-document-pub" version = "0.1.0" dependencies = [ + "anyhow", "collab", "collab-document", "flowy-error", "lib-infra", - "uuid", ] [[package]] @@ -2808,7 +2314,6 @@ dependencies = [ "thiserror 1.0.64", "tokio", "url", - "uuid", "validator 0.18.1", ] @@ -2835,7 +2340,6 @@ dependencies = [ "flowy-notification", "flowy-search-pub", "flowy-sqlite", - "flowy-user-pub", "futures", "lazy_static", "lib-dispatch", @@ -2892,17 +2396,20 @@ dependencies = [ name = "flowy-search" version = "0.1.0" dependencies = [ - "allo-isolate", "async-stream", "bytes", "collab", "collab-folder", - "derive_builder 0.20.2", + "diesel", + "diesel_derives", + "diesel_migrations", "flowy-codegen", "flowy-derive", "flowy-error", "flowy-folder", + "flowy-notification", "flowy-search-pub", + "flowy-sqlite", "flowy-user", "futures", "lib-dispatch", @@ -2910,13 +2417,13 @@ dependencies = [ "protobuf", "serde", "serde_json", - "strsim 0.11.1", + "strsim 0.11.0", "strum_macros 0.26.1", "tantivy", + "tempfile", "tokio", - "tokio-stream", "tracing", - "uuid", + "validator 0.18.1", ] [[package]] @@ -2927,8 +2434,8 @@ dependencies = [ "collab", "collab-folder", "flowy-error", + "futures", "lib-infra", - "uuid", ] [[package]] @@ -2948,8 +2455,8 @@ dependencies = [ "collab-folder", "collab-plugins", "collab-user", + "dashmap 6.0.1", "dotenv", - "flowy-ai", "flowy-ai-pub", "flowy-database-pub", "flowy-document-pub", @@ -2957,25 +2464,33 @@ dependencies = [ "flowy-folder-pub", "flowy-search-pub", "flowy-server-pub", - "flowy-sqlite", "flowy-storage", "flowy-storage-pub", "flowy-user-pub", "futures", "futures-util", + "hex", + "hyper 0.14.27", "lazy_static", + "lib-dispatch", "lib-infra", + "mime_guess", + "postgrest", "rand 0.8.5", + "reqwest 0.11.27", "semver", "serde", "serde_json", "thiserror 1.0.64", "tokio", + "tokio-retry", "tokio-stream", "tokio-util", "tracing", "tracing-subscriber", + "url", "uuid", + "yrs", ] [[package]] @@ -3012,6 +2527,7 @@ name = "flowy-storage" version = "0.1.0" dependencies = [ "allo-isolate", + "anyhow", "async-trait", "bytes", "chrono", @@ -3023,6 +2539,8 @@ dependencies = [ "flowy-notification", "flowy-sqlite", "flowy-storage-pub", + "futures-util", + "fxhash", "lib-dispatch", "lib-infra", "mime_guess", @@ -3050,8 +2568,9 @@ dependencies = [ "mime", "mime_guess", "serde", + "serde_json", "tokio", - "uuid", + "tracing", ] [[package]] @@ -3074,6 +2593,7 @@ dependencies = [ "collab-user", "dashmap 6.0.1", "diesel", + "diesel_derives", "fake", "fancy-regex 0.11.0", "flowy-codegen", @@ -3087,6 +2607,8 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", + "nanoid", + "once_cell", "protobuf", "quickcheck", "quickcheck_macros", @@ -3096,6 +2618,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_repr", "strum", "strum_macros 0.25.2", "tokio", @@ -3110,6 +2633,7 @@ dependencies = [ name = "flowy-user-pub" version = "0.1.0" dependencies = [ + "anyhow", "base64 0.21.5", "chrono", "client-api", @@ -3118,7 +2642,6 @@ dependencies = [ "collab-folder", "flowy-error", "flowy-folder-pub", - "flowy-sqlite", "lib-infra", "serde", "serde_json", @@ -3178,6 +2701,12 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -3196,9 +2725,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -3211,9 +2740,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -3221,15 +2750,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -3238,9 +2767,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -3257,9 +2786,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", @@ -3268,27 +2797,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -3311,6 +2834,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3426,13 +2962,13 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "getrandom 0.2.10", "gotrue-entity", "infra", - "reqwest 0.12.15", + "reqwest 0.12.9", "serde", "serde_json", "tracing", @@ -3441,7 +2977,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "app-error", "jsonwebtoken", @@ -3537,9 +3073,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -3804,15 +3340,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hyperloglogplus" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" -dependencies = [ - "serde", -] - [[package]] name = "iana-time-zone" version = "0.1.61" @@ -4018,12 +3545,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexed_db_futures" version = "0.4.2" @@ -4065,13 +3586,13 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "bytes", "futures", "pin-project", - "reqwest 0.12.15", + "reqwest 0.12.9", "serde", "serde_json", "tokio", @@ -4115,6 +3636,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -4123,17 +3647,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "itertools" version = "0.10.5" @@ -4152,15 +3665,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" @@ -4178,11 +3682,10 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ - "once_cell", "wasm-bindgen", ] @@ -4220,12 +3723,6 @@ dependencies = [ "libc", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.4.0" @@ -4394,23 +3891,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.10" @@ -4433,6 +3913,20 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.12.3" @@ -4561,37 +4055,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" [[package]] -name = "mcp_daemon" -version = "0.2.1" +name = "md-5" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0bdbb83765c69f4bf506d318119a25776dbad54906de9c17c1eae566088100" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "actix-cors", - "actix-web", - "actix-web-lab", - "actix-ws", - "anyhow", - "async-openai", - "async-trait", - "bytes", - "bytestring", - "futures", - "futures-core", - "futures-util", - "jsonwebtoken", - "pin-project-lite", - "reqwest 0.12.15", - "rustls 0.20.9", - "rustls-pemfile 1.0.3", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.21.0", - "tracing", - "url", - "uuid", + "digest", ] [[package]] @@ -4602,19 +4071,14 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "measure_time" -version = "0.9.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" dependencies = [ + "instant", "log", ] -[[package]] -name = "mediatype" -version = "0.19.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" - [[package]] name = "memchr" version = "2.7.4" @@ -4703,18 +4167,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - [[package]] name = "moka" version = "0.12.8" @@ -4808,7 +4260,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio 0.8.9", + "mio", "walkdir", "windows-sys 0.48.0", ] @@ -4869,6 +4321,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.1" @@ -4886,9 +4348,12 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oneshot" -version = "0.1.11" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] [[package]] name = "opaque-debug" @@ -4952,12 +4417,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" dependencies = [ "libc", - "windows-sys 0.59.0", + "winapi", ] [[package]] @@ -4968,9 +4433,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.9.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" dependencies = [ "stable_deref_trait", ] @@ -5311,6 +4776,44 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "postgres-protocol" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +dependencies = [ + "base64 0.21.5", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" +dependencies = [ + "reqwest 0.11.27", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5637,7 +5140,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger 0.8.4", + "env_logger", "log", "rand 0.8.5", ] @@ -5702,7 +5205,7 @@ dependencies = [ "once_cell", "socket2 0.5.5", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5731,6 +5234,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -5776,6 +5292,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -5851,6 +5382,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -5927,12 +5467,6 @@ dependencies = [ "regex-syntax 0.8.4", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.6.29" @@ -5951,6 +5485,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rend" version = "0.4.0" @@ -5987,7 +5530,6 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.7", - "rustls-native-certs", "rustls-pemfile 1.0.3", "serde", "serde_json", @@ -6004,18 +5546,19 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg 0.50.0", + "webpki-roots 0.25.2", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", - "cookie 0.18.1", + "cookie", "cookie_store", "encoding_rs", "futures-core", @@ -6050,7 +5593,6 @@ dependencies = [ "tokio-native-tls", "tokio-rustls 0.26.1", "tokio-util", - "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -6061,22 +5603,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "reqwest-eventsource" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" -dependencies = [ - "eventsource-stream", - "futures-core", - "futures-timer", - "mime", - "nom", - "pin-project-lite", - "reqwest 0.11.27", - "thiserror 1.0.64", -] - [[package]] name = "ring" version = "0.16.20" @@ -6220,18 +5746,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.7" @@ -6258,18 +5772,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.3", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -6377,6 +5879,12 @@ dependencies = [ "parking_lot 0.12.1", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6432,16 +5940,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "serde", - "zeroize", -] - [[package]] name = "security-framework" version = "2.9.2" @@ -6495,18 +5993,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -6524,24 +6022,11 @@ dependencies = [ "syn 2.0.94", ] -[[package]] -name = "serde_html_form" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" -dependencies = [ - "form_urlencoded", - "indexmap 2.1.0", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", @@ -6643,7 +6128,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186" dependencies = [ "anyhow", "app-error", @@ -6656,7 +6141,7 @@ dependencies = [ "gotrue-entity", "infra", "pin-project", - "reqwest 0.12.15", + "reqwest 0.12.9", "serde", "serde_json", "serde_repr", @@ -6733,9 +6218,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "sketches-ddsketch" -version = "0.3.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" dependencies = [ "serde", ] @@ -6837,6 +6322,17 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" @@ -6845,9 +6341,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" @@ -6971,7 +6467,7 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "windows", + "windows 0.52.0", ] [[package]] @@ -7024,15 +6520,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tantivy" -version = "0.24.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21ad8b222d71c57aa979353ed702f0bc6d97e66d368962cbded57fbd19eedd7" +checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856" dependencies = [ "aho-corasick", "arc-swap", "base64 0.22.1", "bitpacking", - "bon", "byteorder", "census", "crc32fast", @@ -7042,20 +6537,20 @@ dependencies = [ "fnv", "fs4", "htmlescape", - "hyperloglogplus", - "itertools 0.14.0", + "itertools 0.12.1", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", + "num_cpus", "once_cell", "oneshot", "rayon", "regex", "rust-stemmers", - "rustc-hash 2.1.0", + "rustc-hash 1.1.0", "serde", "serde_json", "sketches-ddsketch", @@ -7068,7 +6563,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 2.0.9", + "thiserror 1.0.64", "time", "uuid", "winapi", @@ -7076,22 +6571,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.8.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494" +checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.5.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344" +checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" dependencies = [ "downcast-rs", "fastdivide", - "itertools 0.14.0", + "itertools 0.12.1", "serde", "tantivy-bitpacker", "tantivy-common", @@ -7101,9 +6596,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.9.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f" +checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" dependencies = [ "async-trait", "byteorder", @@ -7125,23 +6620,19 @@ dependencies = [ [[package]] name = "tantivy-query-grammar" -version = "0.24.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a" +checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" dependencies = [ "nom", - "serde", - "serde_json", ] [[package]] name = "tantivy-sstable" -version = "0.5.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416" +checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" dependencies = [ - "futures-util", - "itertools 0.14.0", "tantivy-bitpacker", "tantivy-common", "tantivy-fst", @@ -7150,9 +6641,9 @@ dependencies = [ [[package]] name = "tantivy-stacker" -version = "0.5.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1" +checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" dependencies = [ "murmurhash32", "rand_distr", @@ -7161,9 +6652,9 @@ dependencies = [ [[package]] name = "tantivy-tokenizer-api" -version = "0.5.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d" +checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" dependencies = [ "serde", ] @@ -7175,16 +6666,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "tempfile" -version = "3.12.0" +name = "tempdir" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7220,15 +6720,6 @@ dependencies = [ "unic-segment", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.1.17" @@ -7358,21 +6849,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.3", + "mio", + "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -7387,9 +6879,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -7406,6 +6898,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf 0.11.2", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2 0.5.5", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-retry" version = "0.3.0" @@ -7417,17 +6935,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -7471,21 +6978,7 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tungstenite 0.20.1", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "native-tls", - "tokio", - "tokio-native-tls", - "tungstenite 0.21.0", + "tungstenite", ] [[package]] @@ -7580,7 +7073,7 @@ dependencies = [ "prost 0.12.3", "tokio", "tokio-stream", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", "tracing", @@ -7606,32 +7099,17 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" @@ -7817,26 +7295,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.2.0", - "httparse", - "log", - "native-tls", - "rand 0.8.5", - "sha1", - "thiserror 1.0.64", - "url", - "utf-8", -] - [[package]] name = "typenum" version = "1.16.0" @@ -7984,7 +7442,6 @@ dependencies = [ "form_urlencoded", "idna 0.5.0", "percent-encoding", - "serde", ] [[package]] @@ -8061,7 +7518,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" dependencies = [ - "darling 0.20.11", + "darling", "once_cell", "proc-macro-error", "proc-macro2", @@ -8075,7 +7532,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" dependencies = [ - "darling 0.20.11", + "darling", "once_cell", "proc-macro-error2", "proc-macro2", @@ -8134,24 +7591,23 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", - "once_cell", - "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", + "once_cell", "proc-macro2", "quote", "syn 2.0.94", @@ -8172,9 +7628,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8182,9 +7638,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -8195,12 +7651,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" @@ -8250,24 +7703,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "webpki-roots" @@ -8290,6 +7730,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -8321,6 +7771,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.52.0" @@ -8340,39 +7799,34 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ "windows-result", "windows-strings", - "windows-targets 0.53.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-link", + "windows-targets 0.52.6", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-link", + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -8393,15 +7847,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -8426,29 +7871,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8461,12 +7890,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8479,12 +7902,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8497,24 +7914,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8527,12 +7932,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8545,12 +7944,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8563,12 +7956,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -8581,12 +7968,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" version = "0.5.30" @@ -8606,16 +7987,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - [[package]] name = "write16" version = "1.0.0" @@ -8831,6 +8202,15 @@ dependencies = [ "zstd 0.13.2", ] +[[package]] +name = "zip-extensions" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" +dependencies = [ + "zip 2.2.0", +] + [[package]] name = "zopfli" version = "0.8.1" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1561c7ea7d..7a74744060 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -77,10 +77,9 @@ diesel = { version = "2.1.0", features = [ "r2d2", "serde_json", ] } -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" -futures = "0.3.31" +futures = "0.3.29" tokio = "1.38.0" tokio-stream = "0.1.14" async-trait = "0.1.81" @@ -98,18 +97,14 @@ validator = { version = "0.18", features = ["derive"] } tokio-util = "0.7.11" zip = "2.2.0" dashmap = "6.0.1" -derive_builder = "0.20.2" -tantivy = { version = "0.24.0" } -af-plugin = { version = "0.1" } -af-local-ai = { version = "0.1" } # Please using the following command to update the revision id # Current directory: frontend # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } +client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" } [profile.dev] opt-level = 0 @@ -144,19 +139,18 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } +collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" } # Working directory: frontend # To update the commit ID, run: # scripts/tool/update_local_ai_rev.sh new_rev_id # ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" } +appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" } diff --git a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml index ae07268ee9..27d36c310c 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml +++ b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml @@ -12,7 +12,7 @@ serde_json.workspace = true flowy-ast.workspace = true quote = "1.0" -cmd_lib = { version = "1.9.5", optional = true } +cmd_lib = { version = "1.3.0", optional = true } protoc-rust = { version = "2.28.0", optional = true } #protobuf-codegen = { version = "3.7.1" } walkdir = { version = "2", optional = true } diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs index 677e7bcddf..1ddb1bdeb0 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs @@ -76,64 +76,64 @@ pub fn dart_gen(crate_name: &str) { } } -// #[allow(unused_variables)] -// fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { -// // 1. generate the proto files to proto_file_dir -// #[cfg(feature = "proto_gen")] -// let proto_crates = gen_proto_files(crate_name); -// -// for proto_crate in proto_crates { -// let mut proto_file_paths = vec![]; -// let mut file_names = vec![]; -// let proto_file_output_path = proto_crate -// .proto_output_path() -// .to_str() -// .unwrap() -// .to_string(); -// let protobuf_output_path = proto_crate -// .protobuf_crate_path() -// .to_str() -// .unwrap() -// .to_string(); -// -// for (path, file_name) in WalkDir::new(&proto_file_output_path) -// .into_iter() -// .filter_map(|e| e.ok()) -// .map(|e| { -// let path = e.path().to_str().unwrap().to_string(); -// let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); -// (path, file_name) -// }) -// { -// if path.ends_with(".proto") { -// // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project -// println!("cargo:rerun-if-changed={}", path); -// proto_file_paths.push(path); -// file_names.push(file_name); -// } -// } -// let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); -// -// // 2. generate the protobuf files(Dart) -// #[cfg(feature = "ts")] -// generate_ts_protobuf_files( -// dest_folder_name, -// &proto_file_output_path, -// &proto_file_paths, -// &file_names, -// &protoc_bin_path, -// &project, -// ); -// -// // 3. generate the protobuf files(Rust) -// generate_rust_protobuf_files( -// &protoc_bin_path, -// &proto_file_paths, -// &proto_file_output_path, -// &protobuf_output_path, -// ); -// } -// } +#[allow(unused_variables)] +pub fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { + // 1. generate the proto files to proto_file_dir + #[cfg(feature = "proto_gen")] + let proto_crates = gen_proto_files(crate_name); + + for proto_crate in proto_crates { + let mut proto_file_paths = vec![]; + let mut file_names = vec![]; + let proto_file_output_path = proto_crate + .proto_output_path() + .to_str() + .unwrap() + .to_string(); + let protobuf_output_path = proto_crate + .protobuf_crate_path() + .to_str() + .unwrap() + .to_string(); + + for (path, file_name) in WalkDir::new(&proto_file_output_path) + .into_iter() + .filter_map(|e| e.ok()) + .map(|e| { + let path = e.path().to_str().unwrap().to_string(); + let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); + (path, file_name) + }) + { + if path.ends_with(".proto") { + // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project + println!("cargo:rerun-if-changed={}", path); + proto_file_paths.push(path); + file_names.push(file_name); + } + } + let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); + + // 2. generate the protobuf files(Dart) + #[cfg(feature = "ts")] + generate_ts_protobuf_files( + dest_folder_name, + &proto_file_output_path, + &proto_file_paths, + &file_names, + &protoc_bin_path, + &project, + ); + + // 3. generate the protobuf files(Rust) + generate_rust_protobuf_files( + &protoc_bin_path, + &proto_file_paths, + &proto_file_output_path, + &protobuf_output_path, + ); + } +} fn generate_rust_protobuf_files( protoc_bin_path: &Path, diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs index 97a7f5f529..ff51ff952b 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs @@ -153,7 +153,8 @@ pub fn parse_event_crate(event_crate: &TsEventCrate) -> Vec { attrs .iter() .filter(|attr| !attr.attrs.event_attrs.ignore) - .map(|variant| EventASTContext::from(&variant.attrs)) + .enumerate() + .map(|(_index, variant)| EventASTContext::from(&variant.attrs)) .collect::>() }, _ => vec![], diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index 0cbdd41ccd..ab375c77fc 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -19,13 +19,14 @@ serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true +async-trait.workspace = true tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } +futures = "0.3" arc-swap = "1.7" flowy-sqlite = { workspace = true } diesel.workspace = true flowy-error.workspace = true -uuid.workspace = true [features] default = [] diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 223ebacc91..b6e89a5a2d 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -33,10 +33,8 @@ use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::local_storage::CollabPersistenceConfig; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; -use flowy_error::FlowyError; use lib_infra::{if_native, if_wasm}; use tracing::{error, instrument, trace, warn}; -use uuid::Uuid; #[derive(Clone, Debug)] pub enum CollabPluginProviderType { @@ -68,8 +66,8 @@ impl Display for CollabPluginProviderContext { } pub trait WorkspaceCollabIntegrate: Send + Sync { - fn workspace_id(&self) -> Result; - fn device_id(&self) -> Result; + fn workspace_id(&self) -> Result; + fn device_id(&self) -> Result; } pub struct AppFlowyCollabBuilder { @@ -121,15 +119,15 @@ impl AppFlowyCollabBuilder { pub fn collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, ) -> Result { // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match. // This check is crucial in asynchronous code contexts where the workspace_id might change during operation. let actual_workspace_id = self.workspace_integrate.workspace_id()?; - if workspace_id != &actual_workspace_id { + if workspace_id != actual_workspace_id { return Err(anyhow::anyhow!( "workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}", workspace_id, @@ -137,11 +135,12 @@ impl AppFlowyCollabBuilder { )); } let device_id = self.workspace_integrate.device_id()?; + let workspace_id = self.workspace_integrate.workspace_id()?; Ok(CollabObject::new( uid, object_id.to_string(), collab_type, - workspace_id.to_string(), + workspace_id, device_id, )) } @@ -277,7 +276,7 @@ impl AppFlowyCollabBuilder { let collab_db = collab_db.clone(); let device_id = self.workspace_integrate.device_id()?; let collab = tokio::task::spawn_blocking(move || { - let collab = CollabBuilder::new(object.uid, &object.object_id, data_source) + let mut collab = CollabBuilder::new(object.uid, &object.object_id, data_source) .with_device_id(device_id) .build()?; let persistence_config = CollabPersistenceConfig::default(); @@ -285,11 +284,12 @@ impl AppFlowyCollabBuilder { object.uid, object.workspace_id.clone(), object.object_id.to_string(), - object.collab_type, + object.collab_type.clone(), collab_db, persistence_config, ); collab.add_plugin(Box::new(db_plugin)); + collab.initialize(); Ok::<_, Error>(collab) }) .await??; @@ -359,12 +359,7 @@ impl AppFlowyCollabBuilder { { if let Some(collab_db) = collab_db.upgrade() { let write_txn = collab_db.write_txn(); - trace!( - "flush workspace: {} {}:collab:{} to disk", - workspace_id, - collab_type, - object_id - ); + trace!("flush collab:{}-{}-{} to disk", uid, collab_type, object_id); let collab: &Collab = collab.borrow(); let encode_collab = collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab))?; @@ -404,11 +399,11 @@ impl CollabBuilderConfig { pub struct CollabPersistenceImpl { pub db: Weak, pub uid: i64, - pub workspace_id: Uuid, + pub workspace_id: String, } impl CollabPersistenceImpl { - pub fn new(db: Weak, uid: i64, workspace_id: Uuid) -> Self { + pub fn new(db: Weak, uid: i64, workspace_id: String) -> Self { Self { db, uid, @@ -430,11 +425,10 @@ impl CollabPersistence for CollabPersistenceImpl { let object_id = collab.object_id().to_string(); let rocksdb_read = collab_db.read_txn(); - let workspace_id = self.workspace_id.to_string(); - if rocksdb_read.is_exist(self.uid, &workspace_id, &object_id) { + if rocksdb_read.is_exist(self.uid, &self.workspace_id, &object_id) { let mut txn = collab.transact_mut(); - match rocksdb_read.load_doc_with_txn(self.uid, &workspace_id, &object_id, &mut txn) { + match rocksdb_read.load_doc_with_txn(self.uid, &self.workspace_id, &object_id, &mut txn) { Ok(update_count) => { trace!( "did load collab:{}-{} from disk, update_count:{}", @@ -459,7 +453,6 @@ impl CollabPersistence for CollabPersistenceImpl { object_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), CollabError> { - let workspace_id = self.workspace_id.to_string(); let collab_db = self .db .upgrade() @@ -468,7 +461,7 @@ impl CollabPersistence for CollabPersistenceImpl { write_txn .flush_doc( self.uid, - workspace_id.as_str(), + self.workspace_id.as_str(), object_id, encoded_collab.state_vector.to_vec(), encoded_collab.doc_state.to_vec(), diff --git a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs index adb8b72de1..82e993fc49 100644 --- a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs +++ b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs @@ -7,8 +7,6 @@ use flowy_sqlite::{ DBConnection, ExpressionMethods, Identifiable, Insertable, Queryable, }; use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; #[derive(Queryable, Insertable, Identifiable)] #[diesel(table_name = af_collab_metadata)] @@ -45,18 +43,13 @@ pub fn batch_insert_collab_metadata( pub fn batch_select_collab_metadata( mut conn: DBConnection, - object_ids: &[Uuid], -) -> FlowyResult> { - let object_ids = object_ids - .iter() - .map(|id| id.to_string()) - .collect::>(); - + object_ids: &[String], +) -> FlowyResult> { let metadata = dsl::af_collab_metadata - .filter(af_collab_metadata::object_id.eq_any(&object_ids)) + .filter(af_collab_metadata::object_id.eq_any(object_ids)) .load::(&mut conn)? .into_iter() - .flat_map(|m| Uuid::from_str(&m.object_id).map(|v| (v, m))) + .map(|m| (m.object_id.clone(), m)) .collect(); Ok(metadata) } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 969f64e6f9..8ad17c028f 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -44,7 +44,7 @@ collab-integrate = { workspace = true } flowy-derive.workspace = true serde_yaml = "0.9.27" flowy-error = { workspace = true, features = ["impl_from_sqlite", "impl_from_dispatch_error", "impl_from_appflowy_cloud", "impl_from_reqwest", "impl_from_serde", "dart"] } -futures = "0.3.31" +futures = "0.3.26" [features] default = ["dart"] diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 6b2d5af7ba..392d82d858 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -12,13 +12,16 @@ flowy-user-pub = { workspace = true } flowy-folder = { path = "../flowy-folder", features = ["test_helper"] } flowy-folder-pub = { workspace = true } flowy-database2 = { path = "../flowy-database2" } +flowy-database-pub = { workspace = true } flowy-document = { path = "../flowy-document" } +flowy-document-pub = { workspace = true } flowy-ai = { workspace = true } lib-dispatch = { workspace = true } lib-infra = { workspace = true } flowy-server = { path = "../flowy-server" } flowy-server-pub = { workspace = true } flowy-notification = { workspace = true } +anyhow.workspace = true flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-search = { workspace = true } @@ -28,6 +31,8 @@ serde.workspace = true serde_json.workspace = true protobuf.workspace = true tokio = { workspace = true, features = ["full"] } +futures-util = "0.3.26" +thread-id = "3.3.0" bytes.workspace = true nanoid = "0.4.0" tracing.workspace = true @@ -36,17 +41,21 @@ collab = { workspace = true } collab-document = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } +collab-plugins = { workspace = true } collab-entity = { workspace = true } rand = { version = "0.8.5", features = [] } strum = "0.25.0" [dev-dependencies] +dotenv = "0.15.0" +tempdir = "0.3.7" uuid.workspace = true assert-json-diff = "2.0.2" +tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" zip.workspace = true walkdir = "2.5.0" -futures = "0.3.31" +futures = "0.3.30" flowy-ai-pub = { workspace = true } [features] diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index c8c638bc85..c5ac604397 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -1,8 +1,8 @@ use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; use flowy_ai::entities::{ - ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB, - SendChatPayloadPB, + ChatMessageListPB, ChatMessageTypePB, CompleteTextPB, CompleteTextTaskPB, CompletionTypePB, + LoadNextChatMessagePB, LoadPrevChatMessagePB, SendChatPayloadPB, }; use flowy_ai::event_map::AIEvent; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; @@ -87,4 +87,25 @@ impl EventIntegrationTest { .await .parse::() } + + pub async fn complete_text( + &self, + text: &str, + completion_type: CompletionTypePB, + ) -> CompleteTextTaskPB { + let payload = CompleteTextPB { + text: text.to_string(), + completion_type, + stream_port: 0, + object_id: "".to_string(), + rag_ids: vec![], + format: None, + }; + EventBuilder::new(self.clone()) + .event(AIEvent::CompleteText) + .payload(payload) + .async_send() + .await + .parse::() + } } diff --git a/frontend/rust-lib/event-integration-test/src/document/document_event.rs b/frontend/rust-lib/event-integration-test/src/document/document_event.rs index 28fb03e9ed..65b4943f80 100644 --- a/frontend/rust-lib/event-integration-test/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document/document_event.rs @@ -1,6 +1,8 @@ use collab::entity::EncodedCollab; use std::collections::HashMap; +use serde_json::Value; + use flowy_document::entities::*; use flowy_document::event_map::DocumentEvent; use flowy_document::parser::parser_entities::{ @@ -9,8 +11,6 @@ use flowy_document::parser::parser_entities::{ }; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder::event_map::FolderEvent; -use serde_json::Value; -use uuid::Uuid; use crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data}; use crate::event_builder::EventBuilder; @@ -37,7 +37,7 @@ impl DocumentEventTest { Self { event_test: core } } - pub async fn get_encoded_v1(&self, doc_id: &Uuid) -> EncodedCollab { + pub async fn get_encoded_v1(&self, doc_id: &str) -> EncodedCollab { let doc = self .event_test .appflowy_core diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 26515ab5af..fae34e175c 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,5 +1,4 @@ use flowy_folder::view_operation::{GatherEncodedCollab, ViewData}; -use std::str::FromStr; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -11,13 +10,12 @@ use flowy_folder_pub::entities::PublishPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, - RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, UserWorkspaceIdPB, UserWorkspacePB, - WorkspaceMemberInvitationPB, WorkspaceMemberPB, + RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, WorkspaceMemberInvitationPB, + WorkspaceMemberPB, }; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; use flowy_user_pub::entities::Role; -use uuid::Uuid; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; @@ -112,18 +110,6 @@ impl EventIntegrationTest { .parse::() } - pub async fn get_user_workspace(&self, workspace_id: &str) -> UserWorkspacePB { - let payload = UserWorkspaceIdPB { - workspace_id: workspace_id.to_string(), - }; - EventBuilder::new(self.clone()) - .event(UserEvent::GetUserWorkspace) - .payload(payload) - .async_send() - .await - .parse::() - } - pub fn get_folder_search_handler(&self) -> &Arc { self .appflowy_core @@ -137,10 +123,10 @@ impl EventIntegrationTest { let create_view_params = views .into_iter() .map(|view| CreateViewParams { - parent_view_id: Uuid::from_str(&view.parent_view_id).unwrap(), + parent_view_id: view.parent_view_id, name: view.name, layout: view.layout.into(), - view_id: Uuid::from_str(&view.id).unwrap(), + view_id: view.id, initial_data: ViewData::Empty, meta: Default::default(), set_as_current: false, @@ -209,10 +195,9 @@ impl EventIntegrationTest { view_id: &str, layout: ViewLayout, ) -> GatherEncodedCollab { - let view_id = Uuid::from_str(view_id).unwrap(); self .folder_manager - .gather_publish_encode_collab(&view_id, &layout) + .gather_publish_encode_collab(view_id, &layout) .await .unwrap() } @@ -397,3 +382,18 @@ impl ViewTest { Self::new(sdk, ViewLayout::Calendar, data).await } } + +#[allow(dead_code)] +async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { + let request = CreateWorkspacePayloadPB { + name: name.to_owned(), + desc: desc.to_owned(), + }; + + EventBuilder::new(sdk.clone()) + .event(CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .parse::() +} diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index ff0a3847df..573c8b692b 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -1,4 +1,3 @@ -use crate::user_event::TestNotificationSender; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; @@ -8,21 +7,22 @@ use collab_entity::CollabType; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_user::entities::AuthTypePB; +use flowy_server::AppFlowyServer; +use flowy_user::entities::AuthenticatorPB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; use nanoid::nanoid; use semver::Version; use std::env::temp_dir; use std::path::PathBuf; -use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::select; use tokio::task::LocalSet; use tokio::time::sleep; -use uuid::Uuid; + +use crate::user_event::TestNotificationSender; mod chat_event; pub mod database_event; @@ -59,7 +59,7 @@ impl EventIntegrationTest { let clean_path = config.storage_path.clone(); let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); - let authenticator = Arc::new(AtomicU8::new(AuthTypePB::Local as u8)); + let authenticator = Arc::new(AtomicU8::new(AuthenticatorPB::Local as u8)); register_notification_sender(notification_sender.clone()); // In case of dropping the runtime that runs the core, we need to forget the dispatcher @@ -112,25 +112,16 @@ impl EventIntegrationTest { self.appflowy_core.config.application_path.clone() } + pub fn get_server(&self) -> Arc { + self.appflowy_core.server_provider.get_server().unwrap() + } + pub async fn wait_ws_connected(&self) { - if self - .appflowy_core - .server_provider - .get_server() - .unwrap() - .get_ws_state() - .is_connected() - { + if self.get_server().get_ws_state().is_connected() { return; } - let mut ws_state = self - .appflowy_core - .server_provider - .get_server() - .unwrap() - .subscribe_ws_state() - .unwrap(); + let mut ws_state = self.get_server().subscribe_ws_state().unwrap(); loop { select! { _ = sleep(Duration::from_secs(20)) => { @@ -152,19 +143,12 @@ impl EventIntegrationTest { oid: &str, collab_type: CollabType, ) -> Result, FlowyError> { - let server = self.server_provider.get_server()?; - + let server = self.server_provider.get_server().unwrap(); let workspace_id = self.get_current_workspace().await.id; - let oid = Uuid::from_str(oid)?; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_folder_doc_state( - &Uuid::from_str(&workspace_id).unwrap(), - uid, - collab_type, - &oid, - ) + .get_folder_doc_state(&workspace_id, uid, collab_type, oid) .await?; Ok(doc_state) diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 821c3c9a1d..1b82d9b83c 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -17,14 +17,13 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, - OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, - SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, - UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, + AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, + OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, + SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, + UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; -use flowy_user_pub::entities::AuthType; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; @@ -65,7 +64,7 @@ impl EventIntegrationTest { email, name: "appflowy".to_string(), password: password.clone(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: uuid::Uuid::new_v4().to_string(), } .into_bytes() @@ -113,7 +112,7 @@ impl EventIntegrationTest { .await; } - pub fn set_auth_type(&self, auth_type: AuthTypePB) { + pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { self.authenticator.store(auth_type as u8, Ordering::Release); } @@ -140,7 +139,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthTypePB::Server, + authenticator: AuthenticatorPB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +154,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthTypePB::Server, + authenticator: AuthenticatorPB::AppFlowyCloud, }; let user_profile = EventBuilder::new(self.clone()) @@ -190,10 +189,9 @@ impl EventIntegrationTest { } } - pub async fn create_workspace(&self, name: &str, auth_type: AuthType) -> UserWorkspacePB { + pub async fn create_workspace(&self, name: &str) -> UserWorkspacePB { let payload = CreateWorkspacePB { name: name.to_string(), - auth_type: auth_type.into(), }; EventBuilder::new(self.clone()) .event(UserEvent::CreateWorkspace) @@ -280,10 +278,9 @@ impl EventIntegrationTest { .await; } - pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) { - let payload = OpenUserWorkspacePB { + pub async fn open_workspace(&self, workspace_id: &str) { + let payload = UserWorkspaceIdPB { workspace_id: workspace_id.to_string(), - workspace_auth_type: auth_type, }; EventBuilder::new(self.clone()) .event(UserEvent::OpenWorkspace) diff --git a/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs new file mode 100644 index 0000000000..f8c2f08b50 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/chat/ai_tool_test.rs @@ -0,0 +1,19 @@ +use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_ai::entities::CompletionTypePB; + +use std::time::Duration; + +#[tokio::test] +async fn af_cloud_complete_text_test() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + test.af_cloud_sign_up().await; + + let _workspace_id = test.get_current_workspace().await.id; + let _task = test + .complete_text("hello world", CompletionTypePB::MakeLonger) + .await; + + tokio::time::sleep(Duration::from_secs(6)).await; +} diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index aacba827c4..1a5f9356c4 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -3,12 +3,10 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_ai::entities::ChatMessageListPB; use flowy_ai::notification::ChatNotification; -use std::str::FromStr; use flowy_ai_pub::cloud::ChatMessageType; use std::time::Duration; -use uuid::Uuid; #[tokio::test] async fn af_cloud_create_chat_message_test() { @@ -19,19 +17,15 @@ async fn af_cloud_create_chat_message_test() { let current_workspace = test.get_current_workspace().await; let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test - .appflowy_core - .server_provider - .get_server() - .unwrap() - .chat_service(); + let chat_service = test.server_provider.get_server().unwrap().chat_service(); for i in 0..10 { let _ = chat_service .create_question( - &Uuid::from_str(¤t_workspace.id).unwrap(), - &Uuid::from_str(&chat_id).unwrap(), + ¤t_workspace.id, + &chat_id, &format!("hello world {}", i), ChatMessageType::System, + &[], ) .await .unwrap(); @@ -79,19 +73,15 @@ async fn af_cloud_load_remote_system_message_test() { let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test - .appflowy_core - .server_provider - .get_server() - .unwrap() - .chat_service(); + let chat_service = test.server_provider.get_server().unwrap().chat_service(); for i in 0..10 { let _ = chat_service .create_question( - &Uuid::from_str(¤t_workspace.id).unwrap(), - &Uuid::from_str(&chat_id).unwrap(), + ¤t_workspace.id, + &chat_id, &format!("hello server {}", i), ChatMessageType::System, + &[], ) .await .unwrap(); @@ -101,8 +91,10 @@ async fn af_cloud_load_remote_system_message_test() { .notification_sender .subscribe::(&chat_id, ChatNotification::DidLoadLatestChatMessage); + // Previous messages were created by the server, so there are no messages in the local cache. + // It will try to load messages in the background. let all = test.load_next_message(&chat_id, 5, None).await; - assert_eq!(all.messages.len(), 5); + assert!(all.messages.is_empty()); // Wait for the messages to be loaded. let next_back_five = receive_with_timeout(rx, Duration::from_secs(60)) @@ -127,6 +119,7 @@ async fn af_cloud_load_remote_system_message_test() { let first_five_messages = receive_with_timeout(rx, Duration::from_secs(60)) .await .unwrap(); + assert!(!first_five_messages.has_more); assert_eq!(first_five_messages.messages[0].content, "hello server 4"); assert_eq!(first_five_messages.messages[1].content, "hello server 3"); assert_eq!(first_five_messages.messages[2].content, "hello server 2"); diff --git a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs index 773bdab81f..21c16131e9 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/mod.rs @@ -1 +1,2 @@ +mod ai_tool_test; mod chat_message_test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs new file mode 100644 index 0000000000..c1874a5004 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs @@ -0,0 +1,106 @@ +use std::ops::Deref; + +use assert_json_diff::assert_json_eq; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::{Collab, JsonValue, Update}; +use collab_entity::CollabType; + +use event_integration_test::event_builder::EventBuilder; +use flowy_database2::entities::{DatabasePB, DatabaseViewIdPB, RepeatedDatabaseSnapshotPB}; +use flowy_database2::event_map::DatabaseEvent::*; +use flowy_folder::entities::ViewPB; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseDatabaseTest { + pub uuid: String, + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDatabaseTest { + #[allow(dead_code)] + pub async fn new_with_user(uuid: String) -> Option { + let inner = FlowySupabaseTest::new().await?; + inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + Some(Self { uuid, inner }) + } + + pub async fn new_with_new_user() -> Option { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + Some(Self { uuid, inner }) + } + + pub async fn create_database(&self) -> (ViewPB, DatabasePB) { + let current_workspace = self.inner.get_current_workspace().await; + let view = self + .inner + .create_grid(¤t_workspace.id, "my database".to_string(), vec![]) + .await; + let database = self.inner.get_database(&view.id).await; + (view, database) + } + + pub async fn get_collab_json(&self, database_id: &str) -> JsonValue { + let database_editor = self + .database_manager + .get_database(database_id) + .await + .unwrap(); + // let address = Arc::into_raw(database_editor.clone()); + let database = database_editor.get_mutex_database().lock(); + database.get_mutex_collab().to_json_value() + } + + pub async fn get_database_snapshots(&self, view_id: &str) -> RepeatedDatabaseSnapshotPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDatabaseSnapshots) + .payload(DatabaseViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::() + } + + pub async fn get_database_collab_update(&self, database_id: &str) -> Vec { + let workspace_id = self.user_manager.workspace_id().unwrap(); + let cloud_service = self.database_manager.get_cloud_service().clone(); + cloud_service + .get_database_object_doc_state(database_id, CollabType::Database, &workspace_id) + .await + .unwrap() + .unwrap() + } +} + +pub fn assert_database_collab_content( + database_id: &str, + collab_update: &[u8], + expected: JsonValue, +) { + let collab = MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Server, + database_id, + vec![], + false, + )); + collab.lock().with_origin_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update).unwrap(); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json, expected); +} + +impl Deref for FlowySupabaseDatabaseTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs new file mode 100644 index 0000000000..537cdf80d8 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs @@ -0,0 +1,108 @@ +use std::time::Duration; + +use flowy_database2::entities::{ + DatabaseSnapshotStatePB, DatabaseSyncState, DatabaseSyncStatePB, FieldChangesetPB, FieldType, +}; +use flowy_database2::notification::DatabaseNotification::DidUpdateDatabaseSnapshotState; + +use crate::database::supabase_test::helper::{ + assert_database_collab_content, FlowySupabaseDatabaseTest, +}; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn supabase_initial_database_snapshot_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let rx = test + .notification_sender + .subscribe::(&database.id, DidUpdateDatabaseSnapshotState); + + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let expected = test.get_collab_json(&database.id).await; + let snapshots = test.get_database_snapshots(&view.id).await; + assert_eq!(snapshots.items.len(), 1); + assert_database_collab_content(&database.id, &snapshots.items[0].data, expected); + } +} + +#[tokio::test] +async fn supabase_edit_database_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let existing_fields = test.get_all_database_fields(&view.id).await; + for field in existing_fields.items { + if !field.is_primary { + test.delete_field(&view.id, &field.id).await; + } + } + + let field = test.create_field(&view.id, FieldType::Checklist).await; + test + .update_field(FieldChangesetPB { + field_id: field.id.clone(), + view_id: view.id.clone(), + name: Some("hello world".to_string()), + ..Default::default() + }) + .await; + + // wait all updates are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::(&database.id, |pb| { + pb.value == DatabaseSyncState::SyncFinished + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + assert_eq!(test.get_all_database_fields(&view.id).await.items.len(), 2); + let expected = test.get_collab_json(&database.id).await; + let update = test.get_database_collab_update(&database.id).await; + assert_database_collab_content(&database.id, &update, expected); + } +} + +// #[tokio::test] +// async fn cloud_test_supabase_login_sync_database_test() { +// if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { +// let uuid = test.uuid.clone(); +// let (view, database) = test.create_database().await; +// // wait all updates are send to the remote +// let mut rx = test +// .notification_sender +// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// let expected = test.get_collab_json(&database.id).await; +// test.sign_out().await; +// // Drop the test will cause the test resources to be dropped, which will +// // delete the user data folder. +// drop(test); +// +// let new_test = FlowySupabaseDatabaseTest::new_with_user(uuid) +// .await +// .unwrap(); +// // let actual = new_test.get_collab_json(&database.id).await; +// // assert_json_eq!(actual, json!("")); +// +// new_test.open_database(&view.id).await; +// +// // wait all updates are synced from the remote +// let mut rx = new_test +// .notification_sender +// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// +// // when the new sync is finished, the database should be the same as the old one +// let actual = new_test.get_collab_json(&database.id).await; +// assert_json_eq!(actual, expected); +// } +// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs index 7d8ecc9680..04798f044a 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs @@ -66,7 +66,6 @@ async fn af_cloud_upload_big_file_test() { // download the file and then compare the data. let file_service = test - .appflowy_core .server_provider .get_server() .unwrap() diff --git a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs index d9273dbe8b..199c1b43c2 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs @@ -8,8 +8,6 @@ use flowy_document::parser::parser_entities::{ }; use serde_json::{json, Value}; use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; #[tokio::test] async fn get_document_event_test() { @@ -103,8 +101,8 @@ async fn document_size_test() { let s = generate_random_string(string_size); test.insert_index(&view.id, &s, 1, None).await; } - let view_id = Uuid::from_str(&view.id).unwrap(); - let encoded_v1 = test.get_encoded_v1(&view_id).await; + + let encoded_v1 = test.get_encoded_v1(&view.id).await; if encoded_v1.doc_state.len() > max_size { panic!( "The document size is too large. {}", diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs new file mode 100644 index 0000000000..d05e1ef95c --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs @@ -0,0 +1,65 @@ +use std::time::Duration; + +use event_integration_test::document_event::assert_document_data_equal; +use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; + +use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn supabase_document_edit_sync_test() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + + let cloned_test = test.clone(); + let cloned_document_id = document_id.clone(); + test.appflowy_core.dispatcher().spawn(async move { + cloned_test + .insert_document_text(&cloned_document_id, "hello world", 0) + .await; + }); + + // wait all update are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_document_doc_state(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} + +#[tokio::test] +async fn supabase_document_edit_sync_test2() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + + for i in 0..10 { + test + .insert_document_text(&document_id, "hello world", i) + .await; + } + + // wait all update are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_document_doc_state(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs new file mode 100644 index 0000000000..e73273cde6 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs @@ -0,0 +1,118 @@ +// use std::fs::File; +// use std::io::{Cursor, Read}; +// use std::path::Path; +// +// use uuid::Uuid; +// use zip::ZipArchive; +// +// use flowy_storage::StorageObject; +// +// use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; +// +// #[tokio::test] +// async fn supabase_document_upload_text_file_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// let object = StorageObject::from_bytes( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "hello world".as_bytes(), +// "text/plain".to_string(), +// ); +// +// let url = storage_service.create_object(object).await.unwrap(); +// +// let bytes = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// let s = String::from_utf8(bytes.to_vec()).unwrap(); +// assert_eq!(s, "hello world"); +// +// // Delete the text file +// let _ = storage_service.delete_object(url).await; +// } +// } +// +// #[tokio::test] +// async fn supabase_document_upload_zip_file_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// // Upload zip file +// let object = StorageObject::from_file( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "./tests/asset/test.txt.zip", +// ); +// let url = storage_service.create_object(object).await.unwrap(); +// +// // Read zip file +// let zip_data = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// let reader = Cursor::new(zip_data); +// let mut archive = ZipArchive::new(reader).unwrap(); +// for i in 0..archive.len() { +// let mut file = archive.by_index(i).unwrap(); +// let name = file.name().to_string(); +// let mut out = Vec::new(); +// file.read_to_end(&mut out).unwrap(); +// +// if name.starts_with("__MACOSX/") { +// continue; +// } +// assert_eq!(name, "test.txt"); +// assert_eq!(String::from_utf8(out).unwrap(), "hello world"); +// } +// +// // Delete the zip file +// let _ = storage_service.delete_object(url).await; +// } +// } +// #[tokio::test] +// async fn supabase_document_upload_image_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// // Upload zip file +// let object = StorageObject::from_file( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "./tests/asset/logo.png", +// ); +// let url = storage_service.create_object(object).await.unwrap(); +// +// let image_data = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// +// // Read the image file +// let mut file = File::open(Path::new("./tests/asset/logo.png")).unwrap(); +// let mut local_data = Vec::new(); +// file.read_to_end(&mut local_data).unwrap(); +// +// assert_eq!(image_data, local_data); +// +// // Delete the image +// let _ = storage_service.delete_object(url).await; +// } +// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs new file mode 100644 index 0000000000..07ff2d96fe --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs @@ -0,0 +1,49 @@ +use std::ops::Deref; + +use event_integration_test::event_builder::EventBuilder; +use flowy_document::entities::{OpenDocumentPayloadPB, RepeatedDocumentSnapshotMetaPB}; +use flowy_document::event_map::DocumentEvent::GetDocumentSnapshotMeta; +use flowy_folder::entities::ViewPB; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseDocumentTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDocumentTest { + pub async fn new() -> Option { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; + Some(Self { inner }) + } + + pub async fn create_document(&self) -> ViewPB { + let current_workspace = self.inner.get_current_workspace().await; + self + .inner + .create_and_open_document(¤t_workspace.id, "my document".to_string(), vec![]) + .await + } + + #[allow(dead_code)] + pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotMetaPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDocumentSnapshotMeta) + .payload(OpenDocumentPayloadPB { + document_id: view_id.to_string(), + }) + .async_send() + .await + .parse::() + } +} + +impl Deref for FlowySupabaseDocumentTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs new file mode 100644 index 0000000000..165f5fdfc0 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs @@ -0,0 +1,3 @@ +mod edit_test; +mod file_test; +mod helper; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs index 5857190b8b..d0a4a28429 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs @@ -1,8 +1,8 @@ use collab_folder::ViewLayout; + use event_integration_test::EventIntegrationTest; use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB}; use flowy_folder::entities::ViewLayoutPB; -use uuid::Uuid; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; @@ -338,11 +338,11 @@ async fn move_view_event_test() { async fn create_orphan_child_view_and_get_its_ancestors_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); + let view_id = "20240521"; test - .create_orphan_view(name, &view_id, ViewLayoutPB::Grid) + .create_orphan_view(name, view_id, ViewLayoutPB::Grid) .await; - let ancestors = test.get_view_ancestors(&view_id).await; + let ancestors = test.get_view_ancestors(view_id).await; assert_eq!(ancestors.len(), 1); assert_eq!(ancestors[0].name, "Orphan View"); assert_eq!(ancestors[0].id, view_id); diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs index 089310b260..0d5b9bc08c 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs @@ -6,7 +6,6 @@ use flowy_folder::view_operation::GatherEncodedCollab; use flowy_folder_pub::entities::{ PublishDocumentPayload, PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData, }; -use uuid::Uuid; async fn mock_single_document_view_publish_payload( test: &EventIntegrationTest, @@ -141,11 +140,11 @@ async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name #[tokio::test] async fn single_document_get_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; - let view_id = Uuid::new_v4().to_string(); + let view_id = "20240521"; let name = "Orphan View"; - create_single_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, true).await; + create_single_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, true).await; let expect_payload = mock_single_document_view_publish_payload( &test, @@ -161,10 +160,10 @@ async fn single_document_get_publish_view_payload_test() { async fn nested_document_get_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); - create_nested_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, true).await; + let view_id = "20240521"; + create_nested_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, true).await; let expect_payload = mock_nested_document_view_publish_payload( &test, @@ -181,10 +180,10 @@ async fn nested_document_get_publish_view_payload_test() { async fn no_children_publish_view_payload_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); - create_nested_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, false).await; + let view_id = "20240521"; + create_nested_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, false).await; let data = mock_single_document_view_publish_payload( &test, diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs index 2297324c53..09af815d65 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs @@ -4,6 +4,23 @@ use flowy_folder::entities::icon::{UpdateViewIconPayloadPB, ViewIconPB, ViewIcon use flowy_folder::entities::*; use flowy_user::errors::ErrorCode; +#[tokio::test] +async fn create_workspace_event_test() { + let test = EventIntegrationTest::new_anon().await; + let request = CreateWorkspacePayloadPB { + name: "my second workspace".to_owned(), + desc: "".to_owned(), + }; + let view_pb = EventBuilder::new(test) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .parse::(); + + assert_eq!(view_pb.parent_view_id, "my second workspace".to_owned()); +} + // #[tokio::test] // async fn open_workspace_event_test() { // let test = EventIntegrationTest::new_with_guest_user().await; @@ -447,6 +464,35 @@ async fn move_view_event_after_delete_view_test2() { assert_eq!(views[3].name, "My 1-5 view"); } +#[tokio::test] +async fn create_parent_view_with_invalid_name() { + for (name, code) in invalid_workspace_name_test_case() { + let sdk = EventIntegrationTest::new().await; + let request = CreateWorkspacePayloadPB { + name, + desc: "".to_owned(), + }; + assert_eq!( + EventBuilder::new(sdk) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .error() + .unwrap() + .code, + code + ) + } +} + +fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { + vec![ + ("".to_owned(), ErrorCode::WorkspaceNameInvalid), + ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong), + ] +} + #[tokio::test] async fn move_view_across_parent_test() { let test = EventIntegrationTest::new_anon().await; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs index c5566e1b80..01d3a22023 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs @@ -1,3 +1,4 @@ mod local_test; + // #[cfg(feature = "supabase_cloud_test")] // mod supabase_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs new file mode 100644 index 0000000000..a1179ce6cc --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs @@ -0,0 +1,91 @@ +use std::ops::Deref; + +use assert_json_diff::assert_json_eq; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::{Collab, JsonValue, Update}; +use collab_entity::CollabType; +use collab_folder::FolderData; + +use event_integration_test::event_builder::EventBuilder; +use flowy_folder::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB}; +use flowy_folder::event_map::FolderEvent::GetFolderSnapshots; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseFolderTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseFolderTest { + pub async fn new() -> Option { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; + Some(Self { inner }) + } + + pub async fn get_collab_json(&self) -> JsonValue { + let folder = self.folder_manager.get_mutex_folder().lock(); + folder.as_ref().unwrap().to_json_value() + } + + pub async fn get_local_folder_data(&self) -> FolderData { + let folder = self.folder_manager.get_mutex_folder().lock(); + folder.as_ref().unwrap().get_folder_data().unwrap() + } + + pub async fn get_folder_snapshots(&self, workspace_id: &str) -> Vec { + EventBuilder::new(self.inner.deref().clone()) + .event(GetFolderSnapshots) + .payload(WorkspaceIdPB { + value: workspace_id.to_string(), + }) + .async_send() + .await + .parse::() + .items + } + + pub async fn get_collab_update(&self, workspace_id: &str) -> Vec { + let cloud_service = self.folder_manager.get_cloud_service().clone(); + cloud_service + .get_folder_doc_state( + workspace_id, + self.user_manager.user_id().unwrap(), + CollabType::Folder, + workspace_id, + ) + .await + .unwrap() + } +} + +pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], expected: JsonValue) { + if collab_update.is_empty() { + panic!("collab update is empty"); + } + + let collab = MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Server, + workspace_id, + vec![], + false, + )); + collab.lock().with_origin_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update).unwrap(); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json["folder"], expected); +} + +impl Deref for FlowySupabaseFolderTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs new file mode 100644 index 0000000000..5f6a50988a --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs @@ -0,0 +1,122 @@ +use std::time::Duration; + +use assert_json_diff::assert_json_eq; +use serde_json::json; + +use flowy_folder::entities::{FolderSnapshotStatePB, FolderSyncStatePB}; +use flowy_folder::notification::FolderNotification::DidUpdateFolderSnapshotState; + +use crate::folder::supabase_test::helper::{assert_folder_collab_content, FlowySupabaseFolderTest}; +use crate::util::{get_folder_data_from_server, receive_with_timeout}; + +#[tokio::test] +async fn supabase_encrypt_folder_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let secret = test.enable_encryption().await; + + let local_folder_data = test.get_local_folder_data().await; + let workspace_id = test.get_current_workspace().await.id; + let remote_folder_data = get_folder_data_from_server(&uid, &workspace_id, Some(secret)) + .await + .unwrap() + .unwrap(); + + assert_json_eq!(json!(local_folder_data), json!(remote_folder_data)); + } +} + +#[tokio::test] +async fn supabase_decrypt_folder_data_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let secret = Some(test.enable_encryption().await); + let workspace_id = test.get_current_workspace().await.id; + test + .create_view(&workspace_id, "encrypt view".to_string()) + .await; + + let rx = test + .notification_sender + .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); + + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + let folder_data = get_folder_data_from_server(&uid, &workspace_id, secret) + .await + .unwrap() + .unwrap(); + assert_eq!(folder_data.views.len(), 2); + assert_eq!(folder_data.views[1].name, "encrypt view"); + } +} + +#[tokio::test] +#[should_panic] +async fn supabase_decrypt_with_invalid_secret_folder_data_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let _ = Some(test.enable_encryption().await); + let workspace_id = test.get_current_workspace().await.id; + test + .create_view(&workspace_id, "encrypt view".to_string()) + .await; + let rx = test + .notification_sender + .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let _ = get_folder_data_from_server(&uid, &workspace_id, Some("invalid secret".to_string())) + .await + .unwrap(); + } +} +#[tokio::test] +async fn supabase_folder_snapshot_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.id; + let rx = test + .notification_sender + .subscribe::(&workspace_id, DidUpdateFolderSnapshotState); + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let snapshots = test.get_folder_snapshots(&workspace_id).await; + assert_eq!(snapshots.len(), 1); + assert_folder_collab_content(&workspace_id, &snapshots[0].data, expected); + } +} + +#[tokio::test] +async fn supabase_initial_folder_snapshot_test2() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.id; + + test + .create_view(&workspace_id, "supabase test view1".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view2".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view3".to_string()) + .await; + + let rx = test + .notification_sender + .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); + + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let update = test.get_collab_update(&workspace_id).await; + assert_folder_collab_content(&workspace_id, &update, expected); + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/main.rs b/frontend/rust-lib/event-integration-test/tests/main.rs index cf4c1591ac..05f19e9b75 100644 --- a/frontend/rust-lib/event-integration-test/tests/main.rs +++ b/frontend/rust-lib/event-integration-test/tests/main.rs @@ -4,8 +4,6 @@ mod folder; // TODO(Mathias): Enable tests for search // mod search; - -mod sql_test; mod user; pub mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs deleted file mode 100644 index 3294ad26db..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs +++ /dev/null @@ -1,609 +0,0 @@ -use event_integration_test::user_event::use_localhost_af_cloud; -use event_integration_test::EventIntegrationTest; -use flowy_ai_pub::cloud::MessageCursor; -use flowy_ai_pub::persistence::{ - select_answer_where_match_reply_message_id, select_chat_messages, select_message, - select_message_content, total_message_count, upsert_chat_messages, ChatMessageTable, -}; -use uuid::Uuid; - -#[tokio::test] -async fn chat_message_table_insert_select_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id_1 = 1000; - let message_id_2 = 2000; - - // Create test messages - let messages = vec![ - ChatMessageTable { - message_id: message_id_1, - chat_id: chat_id.clone(), - content: "Hello, this is a test message".to_string(), - created_at: 1625097600, // 2021-07-01 - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ChatMessageTable { - message_id: message_id_2, - chat_id: chat_id.clone(), - content: "This is a reply to the test message".to_string(), - created_at: 1625097700, // 2021-07-01, 100 seconds later - author_type: 0, // AI - author_id: "ai".to_string(), - reply_message_id: Some(message_id_1), - metadata: Some(r#"{"source": "test"}"#.to_string()), - is_sync: false, - }, - ]; - - // Test insert_chat_messages - let result = upsert_chat_messages(db_conn, &messages); - assert!( - result.is_ok(), - "Failed to insert chat messages: {:?}", - result - ); - - // Test select_chat_messages - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let messages_result = - select_chat_messages(db_conn, &chat_id, 10, MessageCursor::Offset(0)).unwrap(); - - assert_eq!(messages_result.messages.len(), 2); - assert_eq!(messages_result.total_count, 2); - assert!(!messages_result.has_more); - - // Verify the content of the returned messages - let first_message = messages_result - .messages - .iter() - .find(|m| m.message_id == message_id_1) - .unwrap(); - assert_eq!(first_message.content, "Hello, this is a test message"); - assert_eq!(first_message.author_type, 1); - - let second_message = messages_result - .messages - .iter() - .find(|m| m.message_id == message_id_2) - .unwrap(); - assert_eq!( - second_message.content, - "This is a reply to the test message" - ); - assert_eq!(second_message.reply_message_id, Some(message_id_1)); -} - -#[tokio::test] -async fn chat_message_table_cursor_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create multiple test messages with sequential IDs - let mut messages = Vec::new(); - for i in 1..6 { - messages.push(ChatMessageTable { - message_id: i * 1000, - chat_id: chat_id.clone(), - content: format!("Message {}", i), - created_at: 1625097600 + (i * 100), // Increasing timestamps - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }); - } - - // Insert messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Test MessageCursor::Offset - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_offset = select_chat_messages( - db_conn, - &chat_id, - 2, // Limit to 2 messages - MessageCursor::Offset(0), - ) - .unwrap(); - - assert_eq!(result_offset.messages.len(), 2); - assert!(result_offset.has_more); - - // Test MessageCursor::AfterMessageId - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_after = select_chat_messages( - db_conn, - &chat_id, - 3, // Limit to 3 messages - MessageCursor::AfterMessageId(2000), - ) - .unwrap(); - - assert_eq!(result_after.messages.len(), 3); // Should get message IDs 3000, 4000, 5000 - assert!(result_after.messages.iter().all(|m| m.message_id > 2000)); - - // Test MessageCursor::BeforeMessageId - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_before = select_chat_messages( - db_conn, - &chat_id, - 2, // Limit to 2 messages - MessageCursor::BeforeMessageId(4000), - ) - .unwrap(); - - assert_eq!(result_before.messages.len(), 2); // Should get message IDs 1000, 2000, 3000 - assert!(result_before.messages.iter().all(|m| m.message_id < 4000)); -} - -#[tokio::test] -async fn chat_message_total_count_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create test messages - let messages = vec![ - ChatMessageTable { - message_id: 1001, - chat_id: chat_id.clone(), - content: "Message 1".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ChatMessageTable { - message_id: 1002, - chat_id: chat_id.clone(), - content: "Message 2".to_string(), - created_at: 1625097700, - author_type: 0, - author_id: "ai".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ]; - - // Insert messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Test total_message_count - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 2); - - // Add one more message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let additional_message = ChatMessageTable { - message_id: 1003, - chat_id: chat_id.clone(), - content: "Message 3".to_string(), - created_at: 1625097800, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - upsert_chat_messages(db_conn, &[additional_message]).unwrap(); - - // Verify count increased - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let updated_count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(updated_count, 3); - - // Test count for non-existent chat - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let empty_count = total_message_count(db_conn, "non_existent_chat").unwrap(); - assert_eq!(empty_count, 0); -} - -#[tokio::test] -async fn chat_message_select_message_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 2001; - - // Create test message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: "This is a test message for select_message".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Test select_message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message(db_conn, message_id).unwrap(); - assert!(result.is_some()); - - let retrieved_message = result.unwrap(); - assert_eq!(retrieved_message.message_id, message_id); - assert_eq!(retrieved_message.chat_id, chat_id); - assert_eq!( - retrieved_message.content, - "This is a test message for select_message" - ); - assert_eq!(retrieved_message.author_id, "user_1"); - assert_eq!( - retrieved_message.metadata, - Some(r#"{"test_key": "test_value"}"#.to_string()) - ); - - // Test select_message with non-existent ID - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let non_existent = select_message(db_conn, 9999).unwrap(); - assert!(non_existent.is_none()); -} - -#[tokio::test] -async fn chat_message_select_content_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 3001; - let message_content = "This is the content to retrieve"; - - // Create test message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: message_content.to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Test select_message_content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let content = select_message_content(db_conn, message_id).unwrap(); - assert!(content.is_some()); - assert_eq!(content.unwrap(), message_content); - - // Test with non-existent message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_content = select_message_content(db_conn, 9999).unwrap(); - assert!(no_content.is_none()); -} - -#[tokio::test] -async fn chat_message_reply_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let question_id = 4001; - let answer_id = 4002; - - // Create question and answer messages - let question = ChatMessageTable { - message_id: question_id, - chat_id: chat_id.clone(), - content: "What is the question?".to_string(), - created_at: 1625097600, - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - let answer = ChatMessageTable { - message_id: answer_id, - chat_id: chat_id.clone(), - content: "This is the answer".to_string(), - created_at: 1625097700, - author_type: 0, // AI - author_id: "ai".to_string(), - reply_message_id: Some(question_id), // Link to question - metadata: None, - is_sync: false, - }; - - // Insert messages - upsert_chat_messages(db_conn, &[question, answer]).unwrap(); - - // Test select_message_where_match_reply_message_id - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_answer_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); - - assert!(result.is_some()); - let reply = result.unwrap(); - assert_eq!(reply.message_id, answer_id); - assert_eq!(reply.content, "This is the answer"); - assert_eq!(reply.reply_message_id, Some(question_id)); - - // Test with non-existent reply relation - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_reply = select_answer_where_match_reply_message_id( - db_conn, &chat_id, 9999, // Non-existent question ID - ) - .unwrap(); - - assert!(no_reply.is_none()); - - // Test with wrong chat_id - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let wrong_chat = - select_answer_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); - - assert!(wrong_chat.is_none()); -} - -#[tokio::test] -async fn chat_message_upsert_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 5001; - - // Create initial message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: "Original content".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Check original content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let original = select_message(db_conn, message_id).unwrap().unwrap(); - assert_eq!(original.content, "Original content"); - - // Create updated message with same ID but different content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let updated_message = ChatMessageTable { - message_id, // Same ID - chat_id: chat_id.clone(), - content: "Updated content".to_string(), // New content - created_at: 1625097700, // Updated timestamp - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: Some(1000), // Added reply ID - metadata: Some(r#"{"updated": true}"#.to_string()), - is_sync: false, - }; - - // Upsert message - upsert_chat_messages(db_conn, &[updated_message]).unwrap(); - - // Verify update - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message(db_conn, message_id).unwrap().unwrap(); - assert_eq!(result.content, "Updated content"); - assert_eq!(result.created_at, 1625097700); - assert_eq!(result.reply_message_id, Some(1000)); - assert_eq!(result.metadata, Some(r#"{"updated": true}"#.to_string())); - - // Count should still be 1 (update, not insert) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 1); -} - -#[tokio::test] -async fn chat_message_select_with_large_dataset() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create 100 test messages with sequential IDs - let mut messages = Vec::new(); - for i in 1..=100 { - messages.push(ChatMessageTable { - message_id: i * 100, - chat_id: chat_id.clone(), - content: format!("Message {}", i), - created_at: 1625097600 + (i * 10), // Increasing timestamps - author_type: if i % 2 == 0 { 0 } else { 1 }, // Alternate between AI and User - author_id: if i % 2 == 0 { - "ai".to_string() - } else { - "user_1".to_string() - }, - reply_message_id: if i > 1 && i % 2 == 0 { - Some((i - 1) * 100) - } else { - None - }, // Even messages reply to previous message - metadata: if i % 5 == 0 { - Some(format!(r#"{{"index": {}}}"#, i)) - } else { - None - }, - is_sync: false, - }); - } - - // Insert all 100 messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Verify total count - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 100, "Should have 100 messages in the database"); - - // Test 1: MessageCursor::Offset with small page size - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let page_size = 10; - let result_offset = - select_chat_messages(db_conn, &chat_id, page_size, MessageCursor::Offset(0)).unwrap(); - - assert_eq!( - result_offset.messages.len(), - page_size as usize, - "Should return exactly {page_size} messages" - ); - assert!( - result_offset.has_more, - "Should have more messages available" - ); - assert_eq!(result_offset.total_count, 100, "Total count should be 100"); - - // Test 2: Pagination with offset - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_page2 = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::Offset(page_size), - ) - .unwrap(); - - assert_eq!(result_page2.messages.len(), page_size as usize); - assert!( - result_page2.messages[0].message_id != result_offset.messages[0].message_id, - "Second page should have different messages than first page" - ); - - // Test 3: MessageCursor::AfterMessageId (forward pagination) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let middle_message_id = 5000; // Message ID from the middle - let result_after = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::AfterMessageId(middle_message_id), - ) - .unwrap(); - - assert_eq!(result_after.messages.len(), page_size as usize); - assert!( - result_after - .messages - .iter() - .all(|m| m.message_id > middle_message_id), - "All messages should have ID greater than the cursor" - ); - - // Test 4: MessageCursor::BeforeMessageId (backward pagination) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_before = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::BeforeMessageId(middle_message_id), - ) - .unwrap(); - - assert_eq!(result_before.messages.len(), page_size as usize); - assert!( - result_before - .messages - .iter() - .all(|m| m.message_id < middle_message_id), - "All messages should have ID less than the cursor" - ); - - // Test 5: Large page size (retrieve all) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_all = select_chat_messages( - db_conn, - &chat_id, - 200, // More than we have - MessageCursor::Offset(0), - ) - .unwrap(); - - assert_eq!( - result_all.messages.len(), - 100, - "Should return all 100 messages" - ); - assert!(!result_all.has_more, "Should not have more messages"); - - // Test 6: Empty result when using out of range cursor - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_out_of_range = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::AfterMessageId(10000), // After the last message - ) - .unwrap(); - - assert_eq!( - result_out_of_range.messages.len(), - 0, - "Should return no messages" - ); - assert!( - !result_out_of_range.has_more, - "Should not have more messages" - ); -} diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs deleted file mode 100644 index 773bdab81f..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod chat_message_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 301b6e5a62..718bc1d9af 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -1,7 +1,7 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_user::entities::AuthTypePB; +use flowy_user::entities::AuthenticatorPB; use crate::util::unzip; @@ -72,7 +72,7 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.user_auth_type, AuthTypePB::Server); + assert_eq!(user.authenticator, AuthenticatorPB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; assert_eq!(user_first_level_views.len(), 3); diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs index eaec8f7540..7b31babd0e 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs @@ -1,5 +1,6 @@ use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; +use flowy_user::entities::UpdateUserProfilePayloadPB; use crate::util::generate_test_email; @@ -12,3 +13,29 @@ async fn af_cloud_sign_up_test() { let user = test.af_cloud_sign_in_with_email(&email).await.unwrap(); assert_eq!(user.email, email); } + +#[tokio::test] +async fn af_cloud_update_user_metadata() { + use_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + let user = test.af_cloud_sign_up().await; + + let old_profile = test.get_user_profile().await.unwrap(); + assert_eq!(old_profile.openai_key, "".to_string()); + + test + .update_user_profile(UpdateUserProfilePayloadPB { + id: user.id, + openai_key: Some("new openai key".to_string()), + stability_ai_key: Some("new stability ai key".to_string()), + ..Default::default() + }) + .await; + + let new_profile = test.get_user_profile().await.unwrap(); + assert_eq!(new_profile.openai_key, "new openai key".to_string()); + assert_eq!( + new_profile.stability_ai_key, + "new stability ai key".to_string() + ); +} diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs index 0caa9a6227..9830656bb3 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs @@ -12,7 +12,7 @@ pub async fn get_synced_workspaces( test: &EventIntegrationTest, user_id: i64, ) -> Vec { - let workspaces = test.get_all_workspaces().await.items; + let _workspaces = test.get_all_workspaces().await.items; let sub_id = user_id.to_string(); let rx = test .notification_sender @@ -20,9 +20,8 @@ pub async fn get_synced_workspaces( &sub_id, UserNotification::DidUpdateUserWorkspaces as i32, ); - if let Some(result) = receive_with_timeout(rx, Duration::from_secs(10)).await { - result.items - } else { - workspaces - } + receive_with_timeout(rx, Duration::from_secs(60)) + .await + .unwrap() + .items } diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index d390c0558e..56cf22a4da 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -1,17 +1,15 @@ -use crate::user::af_cloud_test::util::get_synced_workspaces; use collab::core::collab::DataSource::DocStateV1; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::Folder; use event_integration_test::user_event::use_localhost_af_cloud; use event_integration_test::EventIntegrationTest; -use flowy_user::entities::AFRolePB; -use flowy_user_pub::cloud::UserCloudServiceProvider; -use flowy_user_pub::entities::AuthType; use std::time::Duration; use tokio::task::LocalSet; use tokio::time::sleep; +use crate::user::af_cloud_test::util::get_synced_workspaces; + #[tokio::test] async fn af_cloud_workspace_delete() { use_localhost_af_cloud().await; @@ -20,9 +18,7 @@ async fn af_cloud_workspace_delete() { let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 1); - let created_workspace = test - .create_workspace("my second workspace", AuthType::AppFlowyCloud) - .await; + let created_workspace = test.create_workspace("my second workspace").await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); @@ -70,9 +66,7 @@ async fn af_cloud_create_workspace_test() { let first_workspace_id = workspaces[0].workspace_id.as_str(); assert_eq!(workspaces.len(), 1); - let created_workspace = test - .create_workspace("my second workspace", AuthType::AppFlowyCloud) - .await; + let created_workspace = test.create_workspace("my second workspace").await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; @@ -91,12 +85,7 @@ async fn af_cloud_create_workspace_test() { } { // after opening new workspace - test - .open_workspace( - &created_workspace.workspace_id, - created_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&created_workspace.workspace_id).await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); let views = test.folder_read_current_workspace_views().await; @@ -117,7 +106,6 @@ async fn af_cloud_open_workspace_test() { test.create_document("A").await; test.create_document("B").await; let first_workspace = test.get_current_workspace().await; - let first_workspace = test.get_user_workspace(&first_workspace.id).await; let views = test.get_all_workspace_views().await; assert_eq!(views.len(), 4); assert_eq!(views[0].name, default_document_name); @@ -125,17 +113,9 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[2].name, "A"); assert_eq!(views[3].name, "B"); - let user_workspace = test - .create_workspace("second workspace", AuthType::AppFlowyCloud) - .await; - test - .open_workspace( - &user_workspace.workspace_id, - user_workspace.workspace_auth_type, - ) - .await; + let user_workspace = test.create_workspace("second workspace").await; + test.open_workspace(&user_workspace.workspace_id).await; let second_workspace = test.get_current_workspace().await; - let second_workspace = test.get_user_workspace(&second_workspace.id).await; test.create_document("C").await; test.create_document("D").await; @@ -149,23 +129,13 @@ async fn af_cloud_open_workspace_test() { // simulate open workspace and check if the views are correct for i in 0..10 { if i % 2 == 0 { - test - .open_workspace( - &first_workspace.workspace_id, - first_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&first_workspace.id).await; sleep(Duration::from_millis(300)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) .await; } else { - test - .open_workspace( - &second_workspace.workspace_id, - second_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&second_workspace.id).await; sleep(Duration::from_millis(200)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) @@ -173,24 +143,14 @@ async fn af_cloud_open_workspace_test() { } } - test - .open_workspace( - &first_workspace.workspace_id, - first_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&first_workspace.id).await; let views_1 = test.get_all_workspace_views().await; assert_eq!(views_1[0].name, default_document_name); assert_eq!(views_1[1].name, "Shared"); assert_eq!(views_1[2].name, "A"); assert_eq!(views_1[3].name, "B"); - test - .open_workspace( - &second_workspace.workspace_id, - second_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&second_workspace.id).await; let views_2 = test.get_all_workspace_views().await; assert_eq!(views_2[0].name, default_document_name); assert_eq!(views_2[1].name, "Shared"); @@ -246,9 +206,7 @@ async fn af_cloud_different_open_same_workspace_test() { for i in 0..30 { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; - client - .open_workspace(iter_workspace_id, all_workspaces[index].workspace_auth_type) - .await; + client.open_workspace(iter_workspace_id).await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; assert_eq!(views.len(), 2); @@ -291,130 +249,3 @@ async fn af_cloud_different_open_same_workspace_test() { assert_eq!(views.len(), 2, "only get: {:?}", views); // Expecting two views. assert_eq!(views[0].name, "General"); } - -#[tokio::test] -async fn af_cloud_create_local_workspace_test() { - // Setup: Initialize test environment with AppFlowyCloud - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - let _ = test.af_cloud_sign_up().await; - - // Verify initial state: User should have one default workspace - let initial_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - initial_workspaces.len(), - 1, - "User should start with one default workspace" - ); - - // make sure the workspaces order is consistent - // tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; - - // Test: Create a local workspace - let local_workspace = test - .create_workspace("my local workspace", AuthType::Local) - .await; - - // Verify: Local workspace was created correctly - assert_eq!(local_workspace.name, "my local workspace"); - let updated_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - updated_workspaces.len(), - 2, - "Should now have two workspaces" - ); - dbg!(&updated_workspaces); - - // Find local workspace by name instead of using index - let found_local_workspace = updated_workspaces - .iter() - .find(|workspace| workspace.name == "my local workspace") - .expect("Local workspace should exist"); - assert_eq!(found_local_workspace.name, "my local workspace"); - - // Test: Open the local workspace - test - .open_workspace( - &local_workspace.workspace_id, - local_workspace.workspace_auth_type, - ) - .await; - - // Verify: Views in the local workspace - let views = test.get_all_views().await; - assert_eq!( - views.len(), - 2, - "Local workspace should have 2 default views" - ); - assert!( - views - .iter() - .any(|view| view.parent_view_id == local_workspace.workspace_id), - "Views should belong to the local workspace" - ); - - // Verify: Can access all views - for view in views { - test.get_view(&view.id).await; - } - - // Verify: Local workspace members - let members = test - .get_workspace_members(&local_workspace.workspace_id) - .await; - assert_eq!( - members.len(), - 1, - "Local workspace should have only one member" - ); - assert_eq!(members[0].role, AFRolePB::Owner, "User should be the owner"); - - // Test: Create a server workspace - let server_workspace = test - .create_workspace("my server workspace", AuthType::AppFlowyCloud) - .await; - - // Verify: Server workspace was created correctly - assert_eq!(server_workspace.name, "my server workspace"); - let final_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - final_workspaces.len(), - 3, - "Should now have three workspaces" - ); - - dbg!(&final_workspaces); - - // Find workspaces by name instead of using indices - let found_local_workspace = final_workspaces - .iter() - .find(|workspace| workspace.name == "my local workspace") - .expect("Local workspace should exist"); - assert_eq!(found_local_workspace.name, "my local workspace"); - - let found_server_workspace = final_workspaces - .iter() - .find(|workspace| workspace.name == "my server workspace") - .expect("Server workspace should exist"); - assert_eq!(found_server_workspace.name, "my server workspace"); - - // Verify: Server-side only recognizes cloud workspaces (not local ones) - let user_profile = test.get_user_profile().await.unwrap(); - test - .server_provider - .set_server_auth_type(&AuthType::AppFlowyCloud, Some(user_profile.token.clone())) - .unwrap(); - test.server_provider.set_token(&user_profile.token).unwrap(); - - let user_service = test.server_provider.get_server().unwrap().user_service(); - let server_workspaces = user_service - .get_all_workspace(user_profile.id) - .await - .unwrap(); - assert_eq!( - server_workspaces.len(), - 2, - "Server should only see 2 workspaces (the default and server workspace, not the local one)" - ); -} diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 138f6f0258..3cd3733837 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -1,6 +1,6 @@ use event_integration_test::user_event::{login_password, unique_email}; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB}; +use flowy_user::entities::{AuthenticatorPB, SignInPayloadPB, SignUpPayloadPB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -14,7 +14,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -31,6 +31,29 @@ async fn sign_up_with_invalid_email() { ); } } +#[tokio::test] +async fn sign_up_with_long_password() { + let sdk = EventIntegrationTest::new().await; + let request = SignUpPayloadPB { + email: unique_email(), + name: valid_name(), + password: "1234".repeat(100).as_str().to_string(), + auth_type: AuthenticatorPB::Local, + device_id: "".to_string(), + }; + + assert_eq!( + EventBuilder::new(sdk) + .event(SignUp) + .payload(request) + .async_send() + .await + .error() + .unwrap() + .code, + ErrorCode::PasswordTooLong + ); +} #[tokio::test] async fn sign_in_with_invalid_email() { @@ -40,7 +63,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -67,7 +90,7 @@ async fn sign_in_with_invalid_password() { email: unique_email(), password, name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs index 438b120483..00df14e8e1 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs @@ -1,6 +1,6 @@ use crate::user::local_test::helper::*; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use nanoid::nanoid; #[tokio::test] @@ -24,7 +24,9 @@ async fn anon_user_profile_get() { .await .parse::(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.user_auth_type, AuthTypePB::Local); + assert_eq!(user_profile.openai_key, user.openai_key); + assert_eq!(user_profile.stability_ai_key, user.stability_ai_key); + assert_eq!(user_profile.authenticator, AuthenticatorPB::Local); } #[tokio::test] @@ -48,6 +50,31 @@ async fn user_update_with_name() { assert_eq!(user_profile.name, new_name,); } +#[tokio::test] +async fn user_update_with_ai_key() { + let sdk = EventIntegrationTest::new().await; + let user = sdk.init_anon_user().await; + let openai_key = "openai_key".to_owned(); + let stability_ai_key = "stability_ai_key".to_owned(); + let request = UpdateUserProfilePayloadPB::new(user.id) + .openai_key(&openai_key) + .stability_ai_key(&stability_ai_key); + let _ = EventBuilder::new(sdk.clone()) + .event(UpdateUserProfile) + .payload(request) + .async_send() + .await; + + let user_profile = EventBuilder::new(sdk.clone()) + .event(GetUserProfile) + .async_send() + .await + .parse::(); + + assert_eq!(user_profile.openai_key, openai_key,); + assert_eq!(user_profile.stability_ai_key, stability_ai_key,); +} + #[tokio::test] async fn anon_user_update_with_email() { let sdk = EventIntegrationTest::new().await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs index 61833429aa..5a75197a9c 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs @@ -1,9 +1,44 @@ use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; +use flowy_folder::entities::ViewLayoutPB; use std::time::Duration; use crate::util::unzip; +#[tokio::test] +async fn migrate_020_historical_empty_document_test() { + let user_db_path = unzip( + "./tests/user/migration_test/history_user_db", + "020_historical_user_data", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + + let mut views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 1); + + // Check the parent view + let parent_view = views.pop().unwrap(); + assert_eq!(parent_view.layout, ViewLayoutPB::Document); + let data = test.open_document(parent_view.id.clone()).await.data; + assert!(!data.page_id.is_empty()); + assert_eq!(data.blocks.len(), 2); + assert!(!data.meta.children_map.is_empty()); + + // Check the child views of the parent view + let child_views = test.get_view(&parent_view.id).await.child_views; + assert_eq!(child_views.len(), 4); + assert_eq!(child_views[0].layout, ViewLayoutPB::Document); + assert_eq!(child_views[1].layout, ViewLayoutPB::Grid); + assert_eq!(child_views[2].layout, ViewLayoutPB::Calendar); + assert_eq!(child_views[3].layout, ViewLayoutPB::Board); + + let database = test.get_database(&child_views[1].id).await; + assert_eq!(database.fields.len(), 8); + assert_eq!(database.rows.len(), 3); +} + #[tokio::test] async fn migrate_036_fav_v1_workspace_array_test() { // Used to test migration: FavoriteV1AndWorkspaceArrayMigration diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs new file mode 100644 index 0000000000..1b6d5f9cc6 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs @@ -0,0 +1,502 @@ +use std::collections::HashMap; + +use assert_json_diff::assert_json_eq; +use collab_database::rows::database_row_document_id_from_row_id; +use collab_document::blocks::DocumentData; +use collab_entity::CollabType; +use collab_folder::FolderData; +use nanoid::nanoid; +use serde_json::json; + +use event_integration_test::document::document_event::DocumentEventTest; +use event_integration_test::event_builder::EventBuilder; +use event_integration_test::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use flowy_encrypt::decrypt_text; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; +use flowy_user::entities::{ + AuthenticatorPB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB, +}; +use flowy_user::errors::ErrorCode; +use flowy_user::event_map::UserEvent::*; + +use crate::util::*; + +#[tokio::test] +async fn third_party_sign_up_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert( + USER_EMAIL.to_string(), + format!("{}@appflowy.io", nanoid!(6)), + ); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, + }; + + let response = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(payload) + .async_send() + .await + .parse::(); + dbg!(&response); + } +} + +#[tokio::test] +async fn third_party_sign_up_with_encrypt_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + test.supabase_party_sign_up().await; + let user_profile = test.get_user_profile().await.unwrap(); + assert!(user_profile.encryption_sign.is_empty()); + + let secret = test.enable_encryption().await; + let user_profile = test.get_user_profile().await.unwrap(); + assert!(!user_profile.encryption_sign.is_empty()); + + let decryption_sign = decrypt_text(user_profile.encryption_sign, &secret).unwrap(); + assert_eq!(decryption_sign, user_profile.id.to_string()); + } +} + +#[tokio::test] +async fn third_party_sign_up_with_duplicated_uuid() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let email = format!("{}@appflowy.io", nanoid!(6)); + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert(USER_EMAIL.to_string(), email.clone()); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); + + let response_1 = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(OauthSignInPB { + map: map.clone(), + authenticator: AuthenticatorPB::Supabase, + }) + .async_send() + .await + .parse::(); + dbg!(&response_1); + + let response_2 = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(OauthSignInPB { + map: map.clone(), + authenticator: AuthenticatorPB::Supabase, + }) + .async_send() + .await + .parse::(); + assert_eq!(response_1, response_2); + }; +} + +#[tokio::test] +async fn third_party_sign_up_with_duplicated_email() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let email = format!("{}@appflowy.io", nanoid!(6)); + test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await + .unwrap(); + let error = test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await + .err() + .unwrap(); + assert_eq!(error.code, ErrorCode::Conflict); + }; +} + +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new_anon().await; + let old_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + let old_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + let uuid = uuid::Uuid::new_v4().to_string(); + test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + let new_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + assert_eq!(old_views.len(), new_views.len()); + assert_eq!(old_workspace.name, new_workspace.name); + assert_eq!(old_workspace.views.len(), new_workspace.views.len()); + for (index, view) in old_views.iter().enumerate() { + assert_eq!(view.name, new_views[index].name); + assert_eq!(view.layout, new_views[index].layout); + assert_eq!(view.create_time, new_views[index].create_time); + } + } +} + +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new_anon().await; + let uuid = uuid::Uuid::new_v4().to_string(); + + let email = format!("{}@appflowy.io", nanoid!(6)); + // The workspace of the guest will be migrated to the new user with given uuid + let _user_profile = test + .supabase_sign_up_with_uuid(&uuid, Some(email.clone())) + .await + .unwrap(); + let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let old_cloud_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + assert_eq!(old_cloud_views.len(), 1); + assert_eq!(old_cloud_views.first().unwrap().child_views.len(), 1); + + // sign out and then sign in as a guest + test.sign_out().await; + + let _sign_up_context = test.sign_up_as_anon().await; + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + test + .create_view(&new_workspace.id, "new workspace child view".to_string()) + .await; + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + assert_eq!(new_workspace.views.len(), 2); + + // upload to cloud user with given uuid. This time the workspace of the guest will not be merged + // because the cloud user already has a workspace + test + .supabase_sign_up_with_uuid(&uuid, Some(email)) + .await + .unwrap(); + let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let new_cloud_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + assert_eq!(new_cloud_workspace, old_cloud_workspace); + assert_eq!(new_cloud_views, old_cloud_views); + } +} + +#[tokio::test] +async fn get_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let uuid = uuid::Uuid::new_v4().to_string(); + test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + + let result = test.get_user_profile().await; + assert!(result.is_ok()); + } +} + +#[tokio::test] +async fn update_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let uuid = uuid::Uuid::new_v4().to_string(); + let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + test + .update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas")) + .await; + + let new_profile = test.get_user_profile().await.unwrap(); + assert_eq!(new_profile.name, "lucas") + } +} + +#[tokio::test] +async fn update_user_profile_with_existing_email_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let email = format!("{}@appflowy.io", nanoid!(6)); + let _ = test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await; + + let profile = test + .supabase_sign_up_with_uuid( + &uuid::Uuid::new_v4().to_string(), + Some(format!("{}@appflowy.io", nanoid!(6))), + ) + .await + .unwrap(); + let error = test + .update_user_profile( + UpdateUserProfilePayloadPB::new(profile.id) + .name("lucas") + .email(&email), + ) + .await + .unwrap(); + assert_eq!(error.code, ErrorCode::Conflict); + } +} + +#[tokio::test] +async fn migrate_anon_document_on_cloud_signup() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let user_profile = test.sign_up_as_anon().await.user_profile; + + let view = test + .create_view(&user_profile.workspace_id, "My first view".to_string()) + .await; + let document_event = DocumentEventTest::new_with_core(test.clone()); + let block_id = document_event + .insert_index(&view.id, "hello world", 1, None) + .await; + + let _ = test.supabase_party_sign_up().await; + + let workspace_id = test.user_manager.workspace_id().unwrap(); + // After sign up, the documents should be migrated to the cloud + // So, we can get the document data from the cloud + let data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&view.id, &workspace_id) + .await + .unwrap() + .unwrap(); + let block = data.blocks.get(&block_id).unwrap(); + assert_json_eq!( + block.data, + json!({ + "delta": [ + { + "insert": "hello world" + } + ] + }) + ); + } +} + +#[tokio::test] +async fn migrate_anon_data_on_cloud_signup() { + if get_supabase_config().is_some() { + let (cleaner, user_db_path) = unzip( + "./tests/user/supabase_test/history_user_db", + "workspace_sync", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + let user_profile = test.supabase_party_sign_up().await; + + // Get the folder data from remote + let folder_data: FolderData = test + .folder_manager + .get_cloud_service() + .get_folder_data(&user_profile.workspace_id, &user_profile.id) + .await + .unwrap() + .unwrap(); + + let expected_folder_data = expected_workspace_sync_folder_data(); + assert_eq!(folder_data.views.len(), expected_folder_data.views.len()); + + // After migration, the ids of the folder_data should be different from the expected_folder_data + for i in 0..folder_data.views.len() { + let left_view = &folder_data.views[i]; + let right_view = &expected_folder_data.views[i]; + assert_ne!(left_view.id, right_view.id); + assert_ne!(left_view.parent_view_id, right_view.parent_view_id); + assert_eq!(left_view.name, right_view.name); + } + + assert_ne!(folder_data.workspace.id, expected_folder_data.workspace.id); + assert_ne!(folder_data.current_view, expected_folder_data.current_view); + + let database_views = folder_data + .views + .iter() + .filter(|view| view.layout.is_database()) + .collect::>(); + + // Try to load the database from the cloud. + for (i, database_view) in database_views.iter().enumerate() { + let cloud_service = test.database_manager.get_cloud_service(); + let database_id = test + .database_manager + .get_database_id_with_view_id(&database_view.id) + .await + .unwrap(); + let editor = test + .database_manager + .get_database(&database_id) + .await + .unwrap(); + + // The database view setting should be loaded by the view id + let _ = editor + .get_database_view_setting(&database_view.id) + .await + .unwrap(); + + let rows = editor.get_rows(&database_view.id).await.unwrap(); + assert_eq!(rows.len(), 3); + + let workspace_id = test.user_manager.workspace_id().unwrap(); + if i == 0 { + let first_row = rows.first().unwrap().as_ref(); + let icon_url = first_row.meta.icon_url.clone().unwrap(); + assert_eq!(icon_url, "😄"); + + let document_id = database_row_document_id_from_row_id(&first_row.row.id); + let document_data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&document_id, &workspace_id) + .await + .unwrap() + .unwrap(); + + let editor = test + .document_manager + .get_document(&document_id) + .await + .unwrap(); + let expected_document_data = editor.lock().get_document_data().unwrap(); + + // let expected_document_data = test + // .document_manager + // .get_document_data(&document_id) + // .await + // .unwrap(); + assert_eq!(document_data, expected_document_data); + let json = json!(document_data); + assert_eq!( + json["blocks"]["LPMpo0Qaab"]["data"]["delta"][0]["insert"], + json!("Row document") + ); + } + assert!(cloud_service + .get_database_object_doc_state(&database_id, CollabType::Database, &workspace_id) + .await + .is_ok()); + } + + drop(cleaner); + } +} + +fn expected_workspace_sync_folder_data() -> FolderData { + serde_json::from_value::(json!({ + "current_view": "e0811131-9928-4541-a174-20b7553d9e4c", + "current_workspace_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "views": [ + { + "children": { + "items": [ + { + "id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "id": "53333949-c262-447b-8597-107589697059" + } + ] + }, + "created_at": 1693147093, + "desc": "", + "icon": null, + "id": "e203afb3-de5d-458a-8380-33cd788a756e", + "is_favorite": false, + "layout": 0, + "name": "⭐️ Getting started", + "parent_view_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3" + }, + { + "children": { + "items": [ + { + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b" + }, + { + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c" + } + ] + }, + "created_at": 1693147096, + "desc": "", + "icon": null, + "id": "e0811131-9928-4541-a174-20b7553d9e4c", + "is_favorite": false, + "layout": 1, + "name": "database", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147124, + "desc": "", + "icon": null, + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b", + "is_favorite": false, + "layout": 3, + "name": "calendar", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147125, + "desc": "", + "icon": null, + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c", + "is_favorite": false, + "layout": 2, + "name": "board", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147133, + "desc": "", + "icon": null, + "id": "53333949-c262-447b-8597-107589697059", + "is_favorite": false, + "layout": 0, + "name": "document", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ], + "workspaces": [ + { + "child_views": { + "items": [ + { + "id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ] + }, + "created_at": 1693147093, + "id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "name": "Workspace" + } + ] + })) + .unwrap() +} diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md new file mode 100644 index 0000000000..426255b00d --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md @@ -0,0 +1,4 @@ + +## Don't modify the zip files in this folder + +The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues \ No newline at end of file diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip new file mode 100644 index 0000000000..6fd5ca0871 Binary files /dev/null and b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip differ diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs new file mode 100644 index 0000000000..b31fdaa002 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod auth_test; +mod workspace_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs new file mode 100644 index 0000000000..2ccbc9438f --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; +use flowy_folder::entities::WorkspaceSettingPB; +use flowy_folder::event_map::FolderEvent::GetCurrentWorkspaceSetting; +use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; +use flowy_user::entities::{AuthenticatorPB, OauthSignInPB, UserProfilePB}; +use flowy_user::event_map::UserEvent::*; + +use crate::util::*; + +#[tokio::test] +async fn initial_workspace_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert( + USER_EMAIL.to_string(), + format!("{}@gmail.com", uuid::Uuid::new_v4()), + ); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, + }; + + let _ = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(payload) + .async_send() + .await + .parse::(); + + let workspace_settings = EventBuilder::new(test.clone()) + .event(GetCurrentWorkspaceSetting) + .async_send() + .await + .parse::(); + + assert!(workspace_settings.latest_view.is_some()); + dbg!(&workspace_settings); + } +} diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml index 93ea79bcab..3afd04d530 100644 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-ai-pub/Cargo.toml @@ -9,8 +9,6 @@ edition = "2021" lib-infra = { workspace = true } flowy-error = { workspace = true } client-api = { workspace = true } +bytes.workspace = true futures.workspace = true serde_json.workspace = true -serde.workspace = true -uuid.workspace = true -flowy-sqlite = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs index 2292e0f332..98198c8f9f 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs @@ -1,106 +1,49 @@ -use crate::cloud::ai_dto::AvailableModel; +use bytes::Bytes; pub use client_api::entity::ai_dto::{ - AppFlowyOfflineAI, CompleteTextParams, CompletionMessage, CompletionMetadata, CompletionType, - CreateChatContext, CustomPrompt, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, - OutputLayout, RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, + AppFlowyOfflineAI, CompleteTextParams, CompletionMetadata, CompletionType, CreateChatContext, + LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, OutputLayout, RelatedQuestion, + RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, }; pub use client_api::entity::billing_dto::SubscriptionPlan; pub use client_api::entity::chat_dto::{ - ChatMessage, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, MessageCursor, - RepeatedChatMessage, UpdateChatParams, + ChatMessage, ChatMessageMetadata, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, + MessageCursor, RepeatedChatMessage, UpdateChatParams, }; pub use client_api::entity::QuestionStreamValue; -pub use client_api::entity::*; -pub use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; +use client_api::error::AppResponseError; use flowy_error::FlowyError; use futures::stream::BoxStream; use lib_infra::async_trait::async_trait; -use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; -use uuid::Uuid; pub type ChatMessageStream = BoxStream<'static, Result>; pub type StreamAnswer = BoxStream<'static, Result>; -pub type StreamComplete = BoxStream<'static, Result>; - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] -pub struct AIModel { - pub name: String, - pub is_local: bool, - #[serde(default)] - pub desc: String, -} - -impl From for AIModel { - fn from(value: AvailableModel) -> Self { - let desc = value - .metadata - .as_ref() - .and_then(|v| v.get("desc").map(|v| v.as_str().unwrap_or(""))) - .unwrap_or(""); - Self { - name: value.name, - is_local: false, - desc: desc.to_string(), - } - } -} - -impl AIModel { - pub fn server(name: String, desc: String) -> Self { - Self { - name, - is_local: false, - desc, - } - } - - pub fn local(name: String, desc: String) -> Self { - Self { - name, - is_local: true, - desc, - } - } -} - -pub const DEFAULT_AI_MODEL_NAME: &str = "Auto"; -impl Default for AIModel { - fn default() -> Self { - Self { - name: DEFAULT_AI_MODEL_NAME.to_string(), - is_local: false, - desc: "".to_string(), - } - } -} - +pub type StreamComplete = BoxStream<'static, Result>; #[async_trait] pub trait ChatCloudService: Send + Sync + 'static { async fn create_chat( &self, uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - name: &str, - metadata: serde_json::Value, + workspace_id: &str, + chat_id: &str, + rag_ids: Vec, ) -> Result<(), FlowyError>; async fn create_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, message_type: ChatMessageType, + metadata: &[ChatMessageMetadata], ) -> Result; async fn create_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, question_id: i64, metadata: Option, @@ -108,71 +51,74 @@ pub trait ChatCloudService: Send + Sync + 'static { async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + message_id: i64, format: ResponseFormat, - ai_model: Option, ) -> Result; async fn get_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, ) -> Result; async fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, offset: MessageCursor, limit: u64, ) -> Result; async fn get_question_from_answer_id( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, answer_message_id: i64, ) -> Result; async fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - ai_model: Option, ) -> Result; async fn stream_complete( &self, - workspace_id: &Uuid, + workspace_id: &str, params: CompleteTextParams, - ai_model: Option, ) -> Result; - async fn embed_file( + async fn index_file( &self, - workspace_id: &Uuid, + workspace_id: &str, file_path: &Path, - chat_id: &Uuid, + chat_id: &str, metadata: Option>, ) -> Result<(), FlowyError>; + async fn get_local_ai_config(&self, workspace_id: &str) -> Result; + + async fn get_workspace_plan( + &self, + workspace_id: &str, + ) -> Result, FlowyError>; + async fn get_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, ) -> Result; async fn update_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, params: UpdateChatParams, ) -> Result<(), FlowyError>; - async fn get_available_models(&self, workspace_id: &Uuid) -> Result; - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result; + async fn get_available_models(&self, workspace_id: &str) -> Result; } diff --git a/frontend/rust-lib/flowy-ai-pub/src/lib.rs b/frontend/rust-lib/flowy-ai-pub/src/lib.rs index df7dc957e2..1ede32218e 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-ai-pub/src/lib.rs @@ -1,3 +1 @@ pub mod cloud; -pub mod persistence; -pub mod user_service; diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs deleted file mode 100644 index 230e5761d2..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::cloud::MessageCursor; -use client_api::entity::chat_dto::ChatMessage; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::upsert::excluded; -use flowy_sqlite::{ - diesel, insert_into, - query_dsl::*, - schema::{chat_message_table, chat_message_table::dsl}, - DBConnection, ExpressionMethods, Identifiable, Insertable, OptionalExtension, QueryResult, - Queryable, -}; - -#[derive(Queryable, Insertable, Identifiable)] -#[diesel(table_name = chat_message_table)] -#[diesel(primary_key(message_id))] -pub struct ChatMessageTable { - pub message_id: i64, - pub chat_id: String, - pub content: String, - pub created_at: i64, - pub author_type: i64, - pub author_id: String, - pub reply_message_id: Option, - pub metadata: Option, - pub is_sync: bool, -} -impl ChatMessageTable { - pub fn from_message(chat_id: String, message: ChatMessage, is_sync: bool) -> Self { - ChatMessageTable { - message_id: message.message_id, - chat_id, - content: message.content, - created_at: message.created_at.timestamp(), - author_type: message.author.author_type as i64, - author_id: message.author.author_id.to_string(), - reply_message_id: message.reply_message_id, - metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), - is_sync, - } - } -} - -pub fn update_chat_message_is_sync( - mut conn: DBConnection, - chat_id_val: &str, - message_id_val: i64, - is_sync_val: bool, -) -> FlowyResult<()> { - diesel::update(chat_message_table::table) - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .filter(chat_message_table::message_id.eq(message_id_val)) - .set(chat_message_table::is_sync.eq(is_sync_val)) - .execute(&mut *conn)?; - - Ok(()) -} - -pub fn upsert_chat_messages( - mut conn: DBConnection, - new_messages: &[ChatMessageTable], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - for message in new_messages { - let _ = insert_into(chat_message_table::table) - .values(message) - .on_conflict(chat_message_table::message_id) - .do_update() - .set(( - chat_message_table::content.eq(excluded(chat_message_table::content)), - chat_message_table::metadata.eq(excluded(chat_message_table::metadata)), - chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), - chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), - chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), - chat_message_table::reply_message_id.eq(excluded(chat_message_table::reply_message_id)), - )) - .execute(conn)?; - } - Ok::<(), FlowyError>(()) - })?; - - Ok(()) -} - -pub struct ChatMessagesResult { - pub messages: Vec, - pub total_count: i64, - pub has_more: bool, -} - -pub fn select_chat_messages( - mut conn: DBConnection, - chat_id_val: &str, - limit_val: u64, - offset: MessageCursor, -) -> QueryResult { - let mut query = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .into_boxed(); - - match offset { - MessageCursor::AfterMessageId(after_message_id) => { - query = query.filter(chat_message_table::message_id.gt(after_message_id)); - }, - MessageCursor::BeforeMessageId(before_message_id) => { - query = query.filter(chat_message_table::message_id.lt(before_message_id)); - }, - MessageCursor::Offset(offset_val) => { - query = query.offset(offset_val as i64); - }, - MessageCursor::NextBack => {}, - } - - // Get total count before applying limit - let total_count = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .count() - .first::(&mut *conn)?; - - query = query - .order(( - chat_message_table::created_at.desc(), - chat_message_table::message_id.desc(), - )) - .limit(limit_val as i64); - - let messages: Vec = query.load::(&mut *conn)?; - - // Check if there are more messages - let has_more = if let Some(last_message) = messages.last() { - let remaining_count = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .filter(chat_message_table::message_id.lt(last_message.message_id)) - .count() - .first::(&mut *conn)?; - - remaining_count > 0 - } else { - false - }; - - Ok(ChatMessagesResult { - messages, - total_count, - has_more, - }) -} - -pub fn total_message_count(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { - dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .count() - .first::(&mut *conn) -} - -pub fn select_message( - mut conn: DBConnection, - message_id_val: i64, -) -> QueryResult> { - let message = dsl::chat_message_table - .filter(chat_message_table::message_id.eq(message_id_val)) - .first::(&mut *conn) - .optional()?; - Ok(message) -} - -pub fn select_message_content( - mut conn: DBConnection, - message_id_val: i64, -) -> QueryResult> { - let message = dsl::chat_message_table - .filter(chat_message_table::message_id.eq(message_id_val)) - .select(chat_message_table::content) - .first::(&mut *conn) - .optional()?; - Ok(message) -} - -pub fn select_answer_where_match_reply_message_id( - mut conn: DBConnection, - chat_id: &str, - answer_message_id_val: i64, -) -> QueryResult> { - dsl::chat_message_table - .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) - .filter(chat_message_table::chat_id.eq(chat_id)) - .first::(&mut *conn) - .optional() -} diff --git a/frontend/rust-lib/flowy-ai-pub/src/user_service.rs b/frontend/rust-lib/flowy-ai-pub/src/user_service.rs deleted file mode 100644 index e227c977fe..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/user_service.rs +++ /dev/null @@ -1,14 +0,0 @@ -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use uuid::Uuid; - -#[async_trait] -pub trait AIUserService: Send + Sync + 'static { - fn user_id(&self) -> Result; - async fn is_local_model(&self) -> FlowyResult; - fn workspace_id(&self) -> Result; - fn sqlite_connection(&self, uid: i64) -> Result; - fn application_root_dir(&self) -> Result; -} diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index 3a6aaf5898..c2f94d509f 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -12,7 +12,6 @@ flowy-error = { path = "../flowy-error", features = [ "impl_from_dispatch_error", "impl_from_collab_folder", "impl_from_sqlite", - "impl_from_appflowy_cloud", ] } lib-dispatch = { workspace = true } tracing.workspace = true @@ -35,20 +34,21 @@ serde_json = { workspace = true } anyhow = "1.0.86" tokio-stream = "0.1.15" tokio-util = { workspace = true, features = ["full"] } -af-local-ai = { workspace = true } -af-plugin = { workspace = true } -reqwest = { version = "0.11.27", features = ["json"] } +appflowy-local-ai = { version = "0.1.0", features = ["verbose"] } +appflowy-plugin = { version = "0.1.0" } +reqwest = "0.11.27" sha2 = "0.10.7" base64 = "0.21.5" futures-util = "0.3.30" +md5 = "0.7.0" +zip = { workspace = true, features = ["deflate"] } +zip-extensions = "0.8.0" pin-project = "1.1.5" flowy-storage-pub = { workspace = true } collab-integrate.workspace = true - [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] notify = "6.1.1" -af-mcp = { version = "0.1.0" } [dev-dependencies] dotenv = "0.15.0" @@ -61,3 +61,5 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] diff --git a/frontend/rust-lib/flowy-ai/build.rs b/frontend/rust-lib/flowy-ai/build.rs index 77c0c8125b..e9230d3d6d 100644 --- a/frontend/rust-lib/flowy-ai/build.rs +++ b/frontend/rust-lib/flowy-ai/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index 2e9fc7e720..5a784f6db6 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -1,73 +1,65 @@ use crate::chat::Chat; use crate::entities::{ - AIModelPB, AvailableModelsPB, ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, - FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, + ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, FilePB, PredefinedFormatPB, + RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::local_ai::controller::{LocalAIController, LocalAISetting}; -use crate::middleware::chat_service_mw::ChatServiceMiddleware; -use flowy_ai_pub::persistence::read_chat_metadata; +use crate::local_ai::local_llm_chat::LocalAIController; +use crate::middleware::chat_service_mw::AICloudServiceMiddleware; +use crate::persistence::{insert_chat, read_chat_metadata, ChatTable}; use std::collections::HashMap; +use appflowy_plugin::manager::PluginManager; use dashmap::DashMap; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, -}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_ai_pub::cloud::{ChatCloudService, ChatSettings, ModelList, UpdateChatParams}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::DBConnection; use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::util::ai_available_models_key; use collab_integrate::persistence::collab_metadata_sql::{ batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata, }; -use flowy_ai_pub::cloud::ai_dto::AvailableModel; -use flowy_ai_pub::user_service::AIUserService; use flowy_storage_pub::storage::StorageService; use lib_infra::async_trait::async_trait; use lib_infra::util::timestamp; -use serde_json::json; use std::path::PathBuf; -use std::str::FromStr; use std::sync::{Arc, Weak}; -use tokio::sync::RwLock; -use tracing::{error, info, instrument, trace}; -use uuid::Uuid; +use tracing::{error, info, trace}; + +pub trait AIUserService: Send + Sync + 'static { + fn user_id(&self) -> Result; + fn device_id(&self) -> Result; + fn workspace_id(&self) -> Result; + fn sqlite_connection(&self, uid: i64) -> Result; + fn application_root_dir(&self) -> Result; +} /// AIExternalService is an interface for external services that AI plugin can interact with. #[async_trait] pub trait AIExternalService: Send + Sync + 'static { async fn query_chat_rag_ids( &self, - parent_view_id: &Uuid, - chat_id: &Uuid, - ) -> Result, FlowyError>; + parent_view_id: &str, + chat_id: &str, + ) -> Result, FlowyError>; async fn sync_rag_documents( &self, - workspace_id: &Uuid, - rag_ids: Vec, - rag_metadata_map: HashMap, + workspace_id: &str, + rag_ids: Vec, + rag_metadata_map: HashMap, ) -> Result, FlowyError>; - async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError>; + async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError>; } -#[derive(Debug, Default)] -struct ServerModelsCache { - models: Vec, - timestamp: Option, -} - -pub const GLOBAL_ACTIVE_MODEL_KEY: &str = "global_active_model"; - pub struct AIManager { - pub cloud_service_wm: Arc, + pub cloud_service_wm: Arc, pub user_service: Arc, pub external_service: Arc, - chats: Arc>>, - pub local_ai: Arc, - pub store_preferences: Arc, - server_models: Arc>, + chats: Arc>>, + pub local_ai_controller: Arc, + store_preferences: Arc, } impl AIManager { @@ -77,19 +69,22 @@ impl AIManager { store_preferences: Arc, storage_service: Weak, query_service: impl AIExternalService, - local_ai: Arc, ) -> AIManager { let user_service = Arc::new(user_service); - let cloned_local_ai = local_ai.clone(); - tokio::spawn(async move { - cloned_local_ai.observe_plugin_resource().await; - }); - + let plugin_manager = Arc::new(PluginManager::new()); + let local_ai_controller = Arc::new(LocalAIController::new( + plugin_manager.clone(), + store_preferences.clone(), + user_service.clone(), + chat_cloud_service.clone(), + )); let external_service = Arc::new(query_service); - let cloud_service_wm = Arc::new(ChatServiceMiddleware::new( + + // setup local chat service + let cloud_service_wm = Arc::new(AICloudServiceMiddleware::new( user_service.clone(), chat_cloud_service, - local_ai.clone(), + local_ai_controller.clone(), storage_service, )); @@ -97,115 +92,37 @@ impl AIManager { cloud_service_wm, user_service, chats: Arc::new(DashMap::new()), - local_ai, + local_ai_controller, external_service, store_preferences, - server_models: Arc::new(Default::default()), } } - async fn reload_with_workspace_id(&self, workspace_id: &str) { - // Check if local AI is enabled for this workspace and if we're in local mode - let result = self.user_service.is_local_model().await; - if let Err(err) = &result { - if matches!(err.code, ErrorCode::UserNotLogin) { - info!("[AI Manager] User not logged in, skipping local AI reload"); - return; - } - } - - let is_local = result.unwrap_or(false); - let is_enabled = self.local_ai.is_enabled_on_workspace(workspace_id); - let is_running = self.local_ai.is_running(); - info!( - "[AI Manager] Reloading workspace: {}, is_local: {}, is_enabled: {}, is_running: {}", - workspace_id, is_local, is_enabled, is_running - ); - - // Shutdown AI if it's running but shouldn't be (not enabled and not in local mode) - if is_running && !is_enabled && !is_local { - info!("[AI Manager] Local AI is running but not enabled, shutting it down"); - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - // Wait for 5 seconds to allow other services to initialize - // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - if let Err(err) = local_ai.toggle_plugin(false).await { - error!("[AI Manager] failed to shutdown local AI: {:?}", err); - } - }); - return; - } - - // Start AI if it's enabled but not running - if is_enabled && !is_running { - info!("[AI Manager] Local AI is enabled but not running, starting it now"); - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - // Wait for 5 seconds to allow other services to initialize - // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - if let Err(err) = local_ai.toggle_plugin(true).await { - error!("[AI Manager] failed to start local AI: {:?}", err); - } - }); - return; - } - - // Log status for other cases - if is_running { - info!("[AI Manager] Local AI is already running"); - } - } - - #[instrument(skip_all, err)] - pub async fn on_launch_if_authenticated(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; + pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> { + // Ignore following error + let _ = self.local_ai_controller.refresh().await; Ok(()) } - pub async fn initialize_after_sign_in(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; - Ok(()) - } - - pub async fn initialize_after_sign_up(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; - Ok(()) - } - - #[instrument(skip_all, err)] - pub async fn initialize_after_open_workspace( - &self, - workspace_id: &Uuid, - ) -> Result<(), FlowyError> { - self - .reload_with_workspace_id(&workspace_id.to_string()) - .await; - Ok(()) - } - - pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - self.chats.entry(*chat_id).or_insert_with(|| { + pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> { + self.chats.entry(chat_id.to_string()).or_insert_with(|| { Arc::new(Chat::new( self.user_service.user_id().unwrap(), - *chat_id, + chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), )) }); - if self.local_ai.is_running() { - trace!("[AI Plugin] notify open chat: {}", chat_id); - self.local_ai.open_chat(chat_id); + trace!("[AI Plugin] notify open chat: {}", chat_id); + if self.local_ai_controller.is_running() { + self.local_ai_controller.open_chat(chat_id); } let user_service = self.user_service.clone(); let cloud_service_wm = self.cloud_service_wm.clone(); let store_preferences = self.store_preferences.clone(); let external_service = self.external_service.clone(); - let chat_id = *chat_id; + let chat_id = chat_id.to_string(); tokio::spawn(async move { match refresh_chat_setting( &user_service, @@ -216,12 +133,7 @@ impl AIManager { .await { Ok(settings) => { - let rag_ids = settings - .rag_ids - .into_iter() - .flat_map(|r| Uuid::from_str(&r).ok()) - .collect(); - let _ = sync_chat_documents(user_service, external_service, rag_ids).await; + let _ = sync_chat_documents(user_service, external_service, settings.rag_ids).await; }, Err(err) => { error!("failed to refresh chat settings: {}", err); @@ -232,19 +144,19 @@ impl AIManager { Ok(()) } - pub async fn close_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { + pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> { trace!("close chat: {}", chat_id); - self.local_ai.close_chat(chat_id); + self.local_ai_controller.close_chat(chat_id); Ok(()) } - pub async fn delete_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { + pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> { if let Some((_, chat)) = self.chats.remove(chat_id) { chat.close(); - if self.local_ai.is_running() { + if self.local_ai_controller.is_running() { info!("[AI Plugin] notify close chat: {}", chat_id); - self.local_ai.close_chat(chat_id); + self.local_ai_controller.close_chat(chat_id); } } Ok(()) @@ -272,8 +184,8 @@ impl AIManager { pub async fn create_chat( &self, uid: &i64, - parent_view_id: &Uuid, - chat_id: &Uuid, + parent_view_id: &str, + chat_id: &str, ) -> Result, FlowyError> { let workspace_id = self.user_service.workspace_id()?; let rag_ids = self @@ -285,326 +197,70 @@ impl AIManager { self .cloud_service_wm - .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) + .create_chat(uid, &workspace_id, chat_id, rag_ids) .await?; + save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; let chat = Arc::new(Chat::new( - self.user_service.user_id()?, - *chat_id, + self.user_service.user_id().unwrap(), + chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(*chat_id, chat.clone()); + self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) } - pub async fn stream_chat_message( - &self, - params: StreamMessageParams, + pub async fn stream_chat_message<'a>( + &'a self, + params: &'a StreamMessageParams<'a>, ) -> Result { - let chat = self.get_or_create_chat_instance(¶ms.chat_id).await?; - let ai_model = self.get_active_model(¶ms.chat_id.to_string()).await; - let question = chat.stream_chat_message(¶ms, ai_model).await?; + let chat = self.get_or_create_chat_instance(params.chat_id).await?; + let question = chat.stream_chat_message(params).await?; let _ = self .external_service - .notify_did_send_message(¶ms.chat_id, ¶ms.message) + .notify_did_send_message(params.chat_id, params.message) .await; Ok(question) } pub async fn stream_regenerate_response( &self, - chat_id: &Uuid, + chat_id: &str, answer_message_id: i64, answer_stream_port: i64, format: Option, - model: Option, ) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; let question_message_id = chat - .get_question_id_from_answer_id(chat_id, answer_message_id) + .get_question_id_from_answer_id(answer_message_id) .await?; - - let model = model.map_or_else( - || { - self - .store_preferences - .get_object::(&ai_available_models_key(&chat_id.to_string())) - }, - |model| Some(model.into()), - ); chat - .stream_regenerate_response(question_message_id, answer_stream_port, format, model) + .stream_regenerate_response(question_message_id, answer_stream_port, format) .await?; Ok(()) } - pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - let previous_model = self.local_ai.get_local_ai_setting().chat_model_name; - self.local_ai.update_local_ai_setting(setting).await?; - let current_model = self.local_ai.get_local_ai_setting().chat_model_name; - - if previous_model != current_model { - info!( - "[AI Plugin] update global active model, previous: {}, current: {}", - previous_model, current_model - ); - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - let model = AIModel::local(current_model, "".to_string()); - self.update_selected_model(source_key, model).await?; - } - - Ok(()) - } - - async fn get_workspace_select_model(&self) -> FlowyResult { + pub async fn get_available_models(&self) -> FlowyResult { let workspace_id = self.user_service.workspace_id()?; - let model = self - .cloud_service_wm - .get_workspace_default_model(&workspace_id) - .await?; - - if model.is_empty() { - return Ok(DEFAULT_AI_MODEL_NAME.to_string()); - } - Ok(model) - } - - async fn get_server_available_models(&self) -> FlowyResult> { - let workspace_id = self.user_service.workspace_id()?; - let now = timestamp(); - - // First, try reading from the cache with expiration check - let should_fetch = { - let cached_models = self.server_models.read().await; - cached_models.models.is_empty() || cached_models.timestamp.map_or(true, |ts| now - ts >= 300) - }; - - if !should_fetch { - // Cache is still valid, return cached data - let cached_models = self.server_models.read().await; - return Ok(cached_models.models.clone()); - } - - // Cache miss or expired: fetch from the cloud. - match self + let list = self .cloud_service_wm .get_available_models(&workspace_id) - .await - { - Ok(list) => { - let models = list.models; - if let Err(err) = self.update_models_cache(&models, now).await { - error!("Failed to update models cache: {}", err); - } - - Ok(models) - }, - Err(err) => { - error!("Failed to fetch available models: {}", err); - - // Return cached data if available, even if expired - let cached_models = self.server_models.read().await; - if !cached_models.models.is_empty() { - info!("Returning expired cached models due to fetch failure"); - return Ok(cached_models.models.clone()); - } - - // If no cached data, return empty list - Ok(Vec::new()) - }, - } + .await?; + Ok(list) } - async fn update_models_cache( - &self, - models: &[AvailableModel], - timestamp: i64, - ) -> FlowyResult<()> { - match self.server_models.try_write() { - Ok(mut cache) => { - cache.models = models.to_vec(); - cache.timestamp = Some(timestamp); - Ok(()) - }, - Err(_) => { - // Handle lock acquisition failure - Err(FlowyError::internal().with_context("Failed to acquire write lock for models cache")) - }, - } - } - - pub async fn update_selected_model(&self, source: String, model: AIModel) -> FlowyResult<()> { - info!( - "[Model Selection] update {} selected model: {:?}", - source, model - ); - let source_key = ai_available_models_key(&source); - self - .store_preferences - .set_object::(&source_key, &model)?; - - chat_notification_builder(&source, ChatNotification::DidUpdateSelectedModel) - .payload(AIModelPB::from(model)) - .send(); - Ok(()) - } - - #[instrument(skip_all, level = "debug")] - pub async fn toggle_local_ai(&self) -> FlowyResult<()> { - let enabled = self.local_ai.toggle_local_ai().await?; - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - if enabled { - if let Some(name) = self.local_ai.get_plugin_chat_model() { - info!("Set global active model to local ai: {}", name); - let model = AIModel::local(name, "".to_string()); - self.update_selected_model(source_key, model).await?; - } - } else { - info!("Set global active model to default"); - let global_active_model = self.get_workspace_select_model().await?; - let models = self.get_server_available_models().await?; - if let Some(model) = models.into_iter().find(|m| m.name == global_active_model) { - self - .update_selected_model(source_key, AIModel::from(model)) - .await?; - } - } - - Ok(()) - } - - pub async fn get_active_model(&self, source: &str) -> Option { - let mut model = self - .store_preferences - .get_object::(&ai_available_models_key(source)); - - if model.is_none() { - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - model = Some(AIModel::local(local_model, "".to_string())); - } - } - - model - } - - pub async fn get_available_models(&self, source: String) -> FlowyResult { - let is_local_mode = self.user_service.is_local_model().await?; - if is_local_mode { - let setting = self.local_ai.get_local_ai_setting(); - let selected_model = AIModel::local(setting.chat_model_name, "".to_string()); - let models = vec![selected_model.clone()]; - - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::from(selected_model), - }) - } else { - // Build the models list from server models and mark them as non-local. - let mut models: Vec = self - .get_server_available_models() - .await? - .into_iter() - .map(AIModel::from) - .collect(); - - trace!("[Model Selection]: Available models: {:?}", models); - let mut current_active_local_ai_model = None; - - // If user enable local ai, then add local ai model to the list. - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - let model = AIModel::local(local_model, "".to_string()); - current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); - models.push(model); - } - - if models.is_empty() { - return Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::default(), - }); - } - - // Global active model is the model selected by the user in the workspace settings. - let mut server_active_model = self - .get_workspace_select_model() - .await - .map(|m| AIModel::server(m, "".to_string())) - .unwrap_or_else(|_| AIModel::default()); - - trace!( - "[Model Selection] server active model: {:?}", - server_active_model - ); - - let mut user_selected_model = server_active_model.clone(); - // when current select model is deprecated, reset the model to default - if !models.iter().any(|m| m.name == server_active_model.name) { - server_active_model = AIModel::default(); - } - - let source_key = ai_available_models_key(&source); - // We use source to identify user selected model. source can be document id or chat id. - match self.store_preferences.get_object::(&source_key) { - None => { - // when there is selected model and current local ai is active, then use local ai - if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { - user_selected_model = local_ai_model.clone(); - } - }, - Some(mut model) => { - trace!("[Model Selection] user previous select model: {:?}", model); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. - if model.is_local { - if let Some(local_ai_model) = ¤t_active_local_ai_model { - if local_ai_model.name != model.name { - model = local_ai_model.clone(); - } - } - } - - user_selected_model = model; - }, - } - - // If user selected model is not available in the list, use the global active model. - let active_model = models - .iter() - .find(|m| m.name == user_selected_model.name) - .cloned() - .or(Some(server_active_model.clone())); - - // Update the stored preference if a different model is used. - if let Some(ref active_model) = active_model { - if active_model.name != user_selected_model.name { - self - .store_preferences - .set_object::(&source_key, &active_model.clone())?; - } - } - - trace!("[Model Selection] final active model: {:?}", active_model); - let selected_model = AIModelPB::from(active_model.unwrap_or_default()); - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model, - }) - } - } - - pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result, FlowyError> { + pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result, FlowyError> { let chat = self.chats.get(chat_id).as_deref().cloned(); match chat { None => { let chat = Arc::new(Chat::new( - self.user_service.user_id()?, - *chat_id, + self.user_service.user_id().unwrap(), + chat_id.to_string(), self.user_service.clone(), self.cloud_service_wm.clone(), )); - self.chats.insert(*chat_id, chat.clone()); + self.chats.insert(chat_id.to_string(), chat.clone()); Ok(chat) }, Some(chat) => Ok(chat), @@ -628,8 +284,8 @@ impl AIManager { pub async fn load_prev_chat_messages( &self, - chat_id: &Uuid, - limit: u64, + chat_id: &str, + limit: i64, before_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -641,8 +297,8 @@ impl AIManager { pub async fn load_latest_chat_messages( &self, - chat_id: &Uuid, - limit: u64, + chat_id: &str, + limit: i64, after_message_id: Option, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -654,18 +310,17 @@ impl AIManager { pub async fn get_related_questions( &self, - chat_id: &Uuid, + chat_id: &str, message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; - let ai_model = self.get_active_model(&chat_id.to_string()).await; - let resp = chat.get_related_question(message_id, ai_model).await?; + let resp = chat.get_related_question(message_id).await?; Ok(resp) } pub async fn generate_answer( &self, - chat_id: &Uuid, + chat_id: &str, question_message_id: i64, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; @@ -673,19 +328,19 @@ impl AIManager { Ok(resp) } - pub async fn stop_stream(&self, chat_id: &Uuid) -> Result<(), FlowyError> { + pub async fn stop_stream(&self, chat_id: &str) -> Result<(), FlowyError> { let chat = self.get_or_create_chat_instance(chat_id).await?; chat.stop_stream_message().await; Ok(()) } - pub async fn chat_with_file(&self, chat_id: &Uuid, file_path: PathBuf) -> FlowyResult<()> { + pub async fn chat_with_file(&self, chat_id: &str, file_path: PathBuf) -> FlowyResult<()> { let chat = self.get_or_create_chat_instance(chat_id).await?; chat.index_file(file_path).await?; Ok(()) } - pub async fn get_rag_ids(&self, chat_id: &Uuid) -> FlowyResult> { + pub async fn get_rag_ids(&self, chat_id: &str) -> FlowyResult> { if let Some(settings) = self .store_preferences .get_object::(&setting_store_key(chat_id)) @@ -703,8 +358,9 @@ impl AIManager { Ok(settings.rag_ids) } - pub async fn update_rag_ids(&self, chat_id: &Uuid, rag_ids: Vec) -> FlowyResult<()> { + pub async fn update_rag_ids(&self, chat_id: &str, rag_ids: Vec) -> FlowyResult<()> { info!("[Chat] update chat:{} rag ids: {:?}", chat_id, rag_ids); + let workspace_id = self.user_service.workspace_id()?; let update_setting = UpdateChatParams { name: None, @@ -717,6 +373,7 @@ impl AIManager { .await?; let chat_setting_store_key = setting_store_key(chat_id); + if let Some(settings) = self .store_preferences .get_object::(&chat_setting_store_key) @@ -734,10 +391,6 @@ impl AIManager { let user_service = self.user_service.clone(); let external_service = self.external_service.clone(); - let rag_ids = rag_ids - .into_iter() - .flat_map(|r| Uuid::from_str(&r).ok()) - .collect(); sync_chat_documents(user_service, external_service, rag_ids).await?; Ok(()) } @@ -746,7 +399,7 @@ impl AIManager { async fn sync_chat_documents( user_service: Arc, external_service: Arc, - rag_ids: Vec, + rag_ids: Vec, ) -> FlowyResult<()> { if rag_ids.is_empty() { return Ok(()); @@ -776,11 +429,26 @@ async fn sync_chat_documents( Ok(()) } +fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { + let row = ChatTable { + chat_id: chat_id.to_string(), + created_at: timestamp(), + name: "".to_string(), + local_files: "".to_string(), + metadata: "".to_string(), + local_enabled: false, + sync_to_cloud: false, + }; + + insert_chat(conn, &row)?; + Ok(()) +} + async fn refresh_chat_setting( user_service: &Arc, - cloud_service: &Arc, + cloud_service: &Arc, store_preferences: &Arc, - chat_id: &Uuid, + chat_id: &str, ) -> FlowyResult { info!("[Chat] refresh chat:{} setting", chat_id); let workspace_id = user_service.workspace_id()?; @@ -792,7 +460,7 @@ async fn refresh_chat_setting( error!("failed to set chat settings: {}", err); } - chat_notification_builder(chat_id.to_string(), ChatNotification::DidUpdateChatSettings) + chat_notification_builder(chat_id, ChatNotification::DidUpdateChatSettings) .payload(ChatSettingsPB { rag_ids: settings.rag_ids.clone(), }) @@ -801,6 +469,6 @@ async fn refresh_chat_setting( Ok(settings) } -fn setting_store_key(chat_id: &Uuid) -> String { +fn setting_store_key(chat_id: &str) -> String { format!("chat_settings_{}", chat_id) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 052599ef48..17ffb29a0d 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -1,20 +1,20 @@ +use crate::ai_manager::AIUserService; use crate::entities::{ ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, }; -use crate::middleware::chat_service_mw::ChatServiceMiddleware; +use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{chat_notification_builder, ChatNotification}; +use crate::persistence::{ + insert_chat_messages, select_chat_messages, select_message_where_match_reply_message_id, + ChatMessageTable, +}; use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, + ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, }; -use flowy_ai_pub::persistence::{ - select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages, - ChatMessageTable, -}; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::DBConnection; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; @@ -23,7 +23,6 @@ use std::sync::atomic::{AtomicBool, AtomicI64}; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use tracing::{error, instrument, trace}; -use uuid::Uuid; enum PrevMessageState { HasMore, @@ -32,10 +31,10 @@ enum PrevMessageState { } pub struct Chat { - chat_id: Uuid, + chat_id: String, uid: i64, user_service: Arc, - chat_service: Arc, + chat_service: Arc, prev_message_state: Arc>, latest_message_id: Arc, stop_stream: Arc, @@ -45,9 +44,9 @@ pub struct Chat { impl Chat { pub fn new( uid: i64, - chat_id: Uuid, + chat_id: String, user_service: Arc, - chat_service: Arc, + chat_service: Arc, ) -> Chat { Chat { uid, @@ -63,6 +62,18 @@ impl Chat { pub fn close(&self) {} + #[allow(dead_code)] + pub async fn pull_latest_message(&self, limit: i64) { + let latest_message_id = self + .latest_message_id + .load(std::sync::atomic::Ordering::Relaxed); + if latest_message_id > 0 { + let _ = self + .load_remote_chat_messages(limit, None, Some(latest_message_id)) + .await; + } + } + pub async fn stop_stream_message(&self) { self .stop_stream @@ -70,16 +81,16 @@ impl Chat { } #[instrument(level = "info", skip_all, err)] - pub async fn stream_chat_message( - &self, - params: &StreamMessageParams, - preferred_ai_model: Option, + pub async fn stream_chat_message<'a>( + &'a self, + params: &'a StreamMessageParams<'a>, ) -> Result { trace!( - "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, format={:?}", + "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, metadata={:?}, format={:?}", self.chat_id, params.message, params.message_type, + params.metadata, params.format, ); @@ -102,8 +113,9 @@ impl Chat { .create_question( &workspace_id, &self.chat_id, - ¶ms.message, + params.message, params.message_type.clone(), + ¶ms.metadata, ) .await .map_err(|err| { @@ -114,10 +126,20 @@ impl Chat { let _ = question_sink .send(StreamMessage::MessageId(question.message_id).to_string()) .await; + if let Err(err) = self + .chat_service + .index_message_metadata(&self.chat_id, ¶ms.metadata, &mut question_sink) + .await + { + error!("Failed to index file: {}", err); + } + let _ = question_sink.send(StreamMessage::Done.to_string()).await; // Save message to disk - notify_message(&self.chat_id, question.clone())?; + save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?; + let format = params.format.clone().map(Into::into).unwrap_or_default(); + self.stream_response( params.answer_stream_port, answer_stream_buffer, @@ -125,7 +147,6 @@ impl Chat { workspace_id, question.message_id, format, - preferred_ai_model, ); let question_pb = ChatMessagePB::from(question); @@ -138,7 +159,6 @@ impl Chat { question_id: i64, answer_stream_port: i64, format: Option, - ai_model: Option, ) -> FlowyResult<()> { trace!( "[Chat] regenerate and stream chat message: chat_id={}", @@ -164,30 +184,28 @@ impl Chat { workspace_id, question_id, format, - ai_model, ); Ok(()) } - #[allow(clippy::too_many_arguments)] fn stream_response( &self, answer_stream_port: i64, answer_stream_buffer: Arc>, - _uid: i64, - workspace_id: Uuid, + uid: i64, + workspace_id: String, question_id: i64, format: ResponseFormat, - ai_model: Option, ) { let stop_stream = self.stop_stream.clone(); - let chat_id = self.chat_id; + let chat_id = self.chat_id.clone(); let cloud_service = self.chat_service.clone(); + let user_service = self.user_service.clone(); tokio::spawn(async move { let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service - .stream_answer(&workspace_id, &chat_id, question_id, format, ai_model) + .stream_answer(&workspace_id, &chat_id, question_id, format) .await { Ok(mut stream) => { @@ -201,10 +219,8 @@ impl Chat { match message { QuestionStreamValue::Answer { value } => { answer_stream_buffer.lock().await.push_str(&value); - if let Err(err) = answer_sink - .send(StreamMessage::OnData(value).to_string()) - .await - { + // trace!("[Chat] stream answer: {}", value); + if let Err(err) = answer_sink.send(format!("data:{}", value)).await { error!("Failed to stream answer via IsolateSink: {}", err); } }, @@ -212,9 +228,7 @@ impl Chat { if let Ok(s) = serde_json::to_string(&value) { // trace!("[Chat] stream metadata: {}", s); answer_stream_buffer.lock().await.set_metadata(value); - let _ = answer_sink - .send(StreamMessage::Metadata(s).to_string()) - .await; + let _ = answer_sink.send(format!("metadata:{}", s)).await; } }, QuestionStreamValue::KeepAlive => { @@ -223,23 +237,16 @@ impl Chat { } }, Err(err) => { - if err.code == ErrorCode::RequestTimeout || err.code == ErrorCode::Internal { - error!("[Chat] unexpected stream error: {}", err); - let _ = answer_sink.send(StreamMessage::Done.to_string()).await; - } else { - error!("[Chat] failed to stream answer: {}", err); - let _ = answer_sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; - let pb = ChatMessageErrorPB { - chat_id: chat_id.to_string(), - error_message: err.to_string(), - }; - chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) - .payload(pb) - .send(); - return Err(err); - } + error!("[Chat] failed to stream answer: {}", err); + let _ = answer_sink.send(format!("error:{}", err)).await; + let pb = ChatMessageErrorPB { + chat_id: chat_id.clone(), + error_message: err.to_string(), + }; + chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) + .payload(pb) + .send(); + return Err(err); }, } } @@ -256,32 +263,22 @@ impl Chat { let _ = answer_sink .send(format!("AI_MAX_REQUIRED:{}", err.msg)) .await; - } else if err.is_local_ai_not_ready() { - let _ = answer_sink - .send(format!("LOCAL_AI_NOT_READY:{}", err.msg)) - .await; - } else if err.is_local_ai_disabled() { - let _ = answer_sink - .send(format!("LOCAL_AI_DISABLED:{}", err.msg)) - .await; } else { - let _ = answer_sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; + let _ = answer_sink.send(format!("error:{}", err)).await; } let pb = ChatMessageErrorPB { - chat_id: chat_id.to_string(), + chat_id: chat_id.clone(), error_message: err.to_string(), }; - chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) + chat_notification_builder(&chat_id, ChatNotification::StreamChatMessageError) .payload(pb) .send(); return Err(err); }, } - chat_notification_builder(chat_id, ChatNotification::FinishStreaming).send(); + chat_notification_builder(&chat_id, ChatNotification::FinishStreaming).send(); trace!("[Chat] finish streaming"); if answer_stream_buffer.lock().await.is_empty() { @@ -298,7 +295,7 @@ impl Chat { metadata, ) .await?; - notify_message(&chat_id, answer)?; + save_and_notify_message(uid, &chat_id, &user_service, answer)?; Ok::<(), FlowyError>(()) }); } @@ -317,7 +314,7 @@ impl Chat { /// - `before_message_id` is the first message ID in the current chat messages. pub async fn load_prev_chat_messages( &self, - limit: u64, + limit: i64, before_message_id: Option, ) -> Result { trace!( @@ -326,9 +323,9 @@ impl Chat { limit, before_message_id ); - - let offset = before_message_id.map_or(MessageCursor::NextBack, MessageCursor::BeforeMessageId); - let messages = self.load_local_chat_messages(limit, offset).await?; + let messages = self + .load_local_chat_messages(limit, None, before_message_id) + .await?; // If the number of messages equals the limit, then no need to load more messages from remote if messages.len() == limit as usize { @@ -337,7 +334,7 @@ impl Chat { has_more: true, total: 0, }; - chat_notification_builder(self.chat_id, ChatNotification::DidLoadPrevChatMessage) + chat_notification_builder(&self.chat_id, ChatNotification::DidLoadPrevChatMessage) .payload(pb.clone()) .send(); return Ok(pb); @@ -365,7 +362,7 @@ impl Chat { pub async fn load_latest_chat_messages( &self, - limit: u64, + limit: i64, after_message_id: Option, ) -> Result { trace!( @@ -374,8 +371,9 @@ impl Chat { limit, after_message_id, ); - let offset = after_message_id.map_or(MessageCursor::NextBack, MessageCursor::AfterMessageId); - let messages = self.load_local_chat_messages(limit, offset).await?; + let messages = self + .load_local_chat_messages(limit, after_message_id, None) + .await?; trace!( "[Chat] Loaded local chat messages: chat_id={}, messages={}", @@ -397,7 +395,7 @@ impl Chat { async fn load_remote_chat_messages( &self, - limit: u64, + limit: i64, before_message_id: Option, after_message_id: Option, ) -> FlowyResult<()> { @@ -409,7 +407,7 @@ impl Chat { after_message_id ); let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id; + let chat_id = self.chat_id.clone(); let cloud_service = self.chat_service.clone(); let user_service = self.user_service.clone(); let uid = self.uid; @@ -422,7 +420,7 @@ impl Chat { _ => MessageCursor::NextBack, }; match cloud_service - .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit) + .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit as u64) .await { Ok(resp) => { @@ -431,7 +429,6 @@ impl Chat { user_service.sqlite_connection(uid)?, &chat_id, resp.messages.clone(), - true, ) { error!("Failed to save chat:{} messages: {}", chat_id, err); } @@ -458,11 +455,11 @@ impl Chat { } else { *prev_message_state.write().await = PrevMessageState::NoMore; } - chat_notification_builder(chat_id, ChatNotification::DidLoadPrevChatMessage) + chat_notification_builder(&chat_id, ChatNotification::DidLoadPrevChatMessage) .payload(pb) .send(); } else { - chat_notification_builder(chat_id, ChatNotification::DidLoadLatestChatMessage) + chat_notification_builder(&chat_id, ChatNotification::DidLoadLatestChatMessage) .payload(pb) .send(); } @@ -476,21 +473,19 @@ impl Chat { pub async fn get_question_id_from_answer_id( &self, - chat_id: &Uuid, answer_message_id: i64, ) -> Result { let conn = self.user_service.sqlite_connection(self.uid)?; - let local_result = - select_answer_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? - .map(|message| message.message_id); + let local_result = select_message_where_match_reply_message_id(conn, answer_message_id)? + .map(|message| message.message_id); if let Some(message_id) = local_result { return Ok(message_id); } let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id; + let chat_id = self.chat_id.clone(); let cloud_service = self.chat_service.clone(); let question = cloud_service @@ -503,12 +498,11 @@ impl Chat { pub async fn get_related_question( &self, message_id: i64, - ai_model: Option, ) -> Result { let workspace_id = self.user_service.workspace_id()?; let resp = self .chat_service - .get_related_message(&workspace_id, &self.chat_id, message_id, ai_model) + .get_related_message(&workspace_id, &self.chat_id, message_id) .await?; trace!( @@ -533,19 +527,26 @@ impl Chat { .get_answer(&workspace_id, &self.chat_id, question_message_id) .await?; - notify_message(&self.chat_id, answer.clone())?; + save_and_notify_message(self.uid, &self.chat_id, &self.user_service, answer.clone())?; let pb = ChatMessagePB::from(answer); Ok(pb) } async fn load_local_chat_messages( &self, - limit: u64, - offset: MessageCursor, + limit: i64, + after_message_id: Option, + before_message_id: Option, ) -> Result, FlowyError> { let conn = self.user_service.sqlite_connection(self.uid)?; - let rows = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?.messages; - let messages = rows + let records = select_chat_messages( + conn, + &self.chat_id, + limit, + after_message_id, + before_message_id, + )?; + let messages = records .into_iter() .map(|record| ChatMessagePB { message_id: record.message_id, @@ -582,7 +583,7 @@ impl Chat { ); self .chat_service - .embed_file( + .index_file( &self.user_service.workspace_id()?, &file_path, &self.chat_id, @@ -602,9 +603,8 @@ impl Chat { fn save_chat_message_disk( conn: DBConnection, - chat_id: &Uuid, + chat_id: &str, messages: Vec, - is_sync: bool, ) -> FlowyResult<()> { let records = messages .into_iter() @@ -616,11 +616,10 @@ fn save_chat_message_disk( author_type: message.author.author_type as i64, author_id: message.author.author_id.to_string(), reply_message_id: message.reply_message_id, - metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), - is_sync, + metadata: Some(serde_json::to_string(&message.meta_data).unwrap_or_default()), }) .collect::>(); - upsert_chat_messages(conn, &records)?; + insert_chat_messages(conn, &records)?; Ok(()) } @@ -657,8 +656,18 @@ impl StringBuffer { } } -pub(crate) fn notify_message(chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { +pub(crate) fn save_and_notify_message( + uid: i64, + chat_id: &str, + user_service: &Arc, + message: ChatMessage, +) -> Result<(), FlowyError> { trace!("[Chat] save answer: answer={:?}", message); + save_chat_message_disk( + user_service.sqlite_connection(uid)?, + chat_id, + vec![message.clone()], + )?; let pb = ChatMessagePB::from(message); chat_notification_builder(chat_id, ChatNotification::DidReceiveChatMessage) .payload(pb) diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs index ffdccd0680..0dd145d853 100644 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ b/frontend/rust-lib/flowy-ai/src/completion.rs @@ -1,23 +1,19 @@ +use crate::ai_manager::AIUserService; use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB}; use allo_isolate::Isolate; -use std::str::FromStr; use dashmap::DashMap; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionStreamValue, - CompletionType, CustomPrompt, + ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionType, }; use flowy_error::{FlowyError, FlowyResult}; use futures::{SinkExt, StreamExt}; use lib_infra::isolate_stream::IsolateSink; -use crate::stream_message::StreamMessage; -use flowy_ai_pub::user_service::AIUserService; use std::sync::{Arc, Weak}; use tokio::select; -use tracing::{error, info}; -use uuid::Uuid; +use tracing::info; pub struct AICompletion { tasks: Arc>>, @@ -40,30 +36,14 @@ impl AICompletion { pub async fn create_complete_task( &self, complete: CompleteTextPB, - preferred_model: Option, ) -> FlowyResult { - if matches!(complete.completion_type, CompletionTypePB::CustomPrompt) - && complete.custom_prompt.is_none() - { - return Err( - FlowyError::invalid_data() - .with_context("custom_prompt is required when completion_type is CustomPrompt"), - ); - } - let workspace_id = self .user_service .upgrade() .ok_or_else(FlowyError::internal)? .workspace_id()?; let (tx, rx) = tokio::sync::mpsc::channel(1); - let task = CompletionTask::new( - workspace_id, - complete, - preferred_model, - self.cloud_service.clone(), - rx, - ); + let task = CompletionTask::new(workspace_id, complete, self.cloud_service.clone(), rx); let task_id = task.task_id.clone(); self.tasks.insert(task_id.clone(), tx); @@ -79,19 +59,17 @@ impl AICompletion { } pub struct CompletionTask { - workspace_id: Uuid, + workspace_id: String, task_id: String, stop_rx: tokio::sync::mpsc::Receiver<()>, context: CompleteTextPB, cloud_service: Weak, - preferred_model: Option, } impl CompletionTask { pub fn new( - workspace_id: Uuid, + workspace_id: String, context: CompleteTextPB, - preferred_model: Option, cloud_service: Weak, stop_rx: tokio::sync::mpsc::Receiver<()>, ) -> Self { @@ -101,7 +79,6 @@ impl CompletionTask { context, cloud_service, stop_rx, - preferred_model, } } @@ -118,89 +95,64 @@ impl CompletionTask { CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, CompletionTypePB::ExplainSelected => CompletionType::Explain, CompletionTypePB::UserQuestion => CompletionType::UserQuestion, - CompletionTypePB::CustomPrompt => CompletionType::CustomPrompt, }; let _ = sink.send("start:".to_string()).await; - let completion_history = Some(self.context.history.iter().map(Into::into).collect()); - let format = self.context.format.map(Into::into).unwrap_or_default(); - if let Ok(object_id) = Uuid::from_str(&self.context.object_id) { - let params = CompleteTextParams { - text: self.context.text, - completion_type: Some(complete_type), - metadata: Some(CompletionMetadata { - object_id, - workspace_id: Some(self.workspace_id), - rag_ids: Some(self.context.rag_ids), - completion_history, - custom_prompt: self - .context - .custom_prompt - .map(|v| CustomPrompt { system: v }), - }), - format, - }; + let params = CompleteTextParams { + text: self.context.text, + completion_type: Some(complete_type), + custom_prompt: None, + metadata: Some(CompletionMetadata { + object_id: self.context.object_id, + workspace_id: Some(self.workspace_id.clone()), + rag_ids: Some(self.context.rag_ids), + }), + format: self.context.format.map(Into::into).unwrap_or_default(), + }; - info!("start completion: {:?}", params); - match cloud_service - .stream_complete(&self.workspace_id, params, self.preferred_model) - .await - { - Ok(mut stream) => loop { - select! { - _ = self.stop_rx.recv() => { - return; - }, - result = stream.next() => { + info!("start completion: {:?}", params); + match cloud_service + .stream_complete(&self.workspace_id, params) + .await + { + Ok(mut stream) => loop { + select! { + _ = self.stop_rx.recv() => { + return; + }, + result = stream.next() => { match result { - Some(Ok(data)) => { - match data { - CompletionStreamValue::Answer{ value } => { - let _ = sink.send(format!("data:{}", value)).await; - } - CompletionStreamValue::Comment{ value } => { - let _ = sink.send(format!("comment:{}", value)).await; - } - } - }, - Some(Err(error)) => { - handle_error(&mut sink, error).await; - return; - }, - None => { - let _ = sink.send(format!("finish:{}", self.task_id)).await; - return; - }, + Some(Ok(data)) => { + let s = String::from_utf8(data.to_vec()).unwrap_or_default(); + let _ = sink.send(format!("data:{}", s)).await; + }, + Some(Err(error)) => { + handle_error(&mut sink, error).await; + return; + }, + None => { + let _ = sink.send(format!("finish:{}", self.task_id)).await; + return; + }, } - } - } - }, - Err(error) => { - handle_error(&mut sink, error).await; - }, - } - } else { - error!("Invalid uuid: {}", self.context.object_id); + } + } + }, + Err(error) => { + handle_error(&mut sink, error).await; + }, } } }); } } -async fn handle_error(sink: &mut IsolateSink, err: FlowyError) { - if err.is_ai_response_limit_exceeded() { +async fn handle_error(sink: &mut IsolateSink, error: FlowyError) { + if error.is_ai_response_limit_exceeded() { let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await; - } else if err.is_ai_image_response_limit_exceeded() { + } else if error.is_ai_image_response_limit_exceeded() { let _ = sink.send("AI_IMAGE_RESPONSE_LIMIT".to_string()).await; - } else if err.is_ai_max_required() { - let _ = sink.send(format!("AI_MAX_REQUIRED:{}", err.msg)).await; - } else if err.is_local_ai_not_ready() { - let _ = sink.send(format!("LOCAL_AI_NOT_READY:{}", err.msg)).await; - } else if err.is_local_ai_disabled() { - let _ = sink.send(format!("LOCAL_AI_DISABLED:{}", err.msg)).await; } else { - let _ = sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; + let _ = sink.send(format!("error:{}", error)).await; } } diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 5a4aecbbd7..7b1cb66593 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -1,14 +1,14 @@ -use crate::local_ai::controller::LocalAISetting; -use crate::local_ai::resource::PendingResource; -use af_plugin::core::plugin::RunningState; +use crate::local_ai::local_llm_chat::LLMModelInfo; +use appflowy_plugin::core::plugin::RunningState; +use std::collections::HashMap; + +use crate::local_ai::local_llm_resource::PendingResource; use flowy_ai_pub::cloud::{ - AIModel, ChatMessage, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, + ChatMessage, ChatMessageMetadata, ChatMessageType, LLMModel, OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use lib_infra::validator_fn::required_not_empty_str; -use std::collections::HashMap; -use uuid::Uuid; use validator::Validate; #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -70,16 +70,20 @@ pub struct StreamChatPayloadPB { #[pb(index = 6, one_of)] pub format: Option, + + #[pb(index = 7)] + pub metadata: Vec, } #[derive(Default, Debug)] -pub struct StreamMessageParams { - pub chat_id: Uuid, - pub message: String, +pub struct StreamMessageParams<'a> { + pub chat_id: &'a str, + pub message: &'a str, pub message_type: ChatMessageType, pub answer_stream_port: i64, pub question_stream_port: i64, pub format: Option, + pub metadata: Vec, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -96,9 +100,6 @@ pub struct RegenerateResponsePB { #[pb(index = 4, one_of)] pub format: Option, - - #[pb(index = 5, one_of)] - pub model: Option, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -181,80 +182,10 @@ pub struct ChatMessageListPB { pub total: i64, } -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ServerAvailableModelsPB { - #[pb(index = 1)] - pub models: Vec, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AvailableModelPB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2)] - pub is_default: bool, - - #[pb(index = 3)] - pub desc: String, -} - #[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct AvailableModelsQueryPB { +pub struct ModelConfigPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub source: String, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct UpdateSelectedModelPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub source: String, - - #[pb(index = 2)] - pub selected_model: AIModelPB, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AvailableModelsPB { - #[pb(index = 1)] - pub models: Vec, - - #[pb(index = 2)] - pub selected_model: AIModelPB, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AIModelPB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2)] - pub is_local: bool, - - #[pb(index = 3)] - pub desc: String, -} - -impl From for AIModelPB { - fn from(model: AIModel) -> Self { - Self { - name: model.name, - is_local: model.is_local, - desc: model.desc, - } - } -} - -impl From for AIModel { - fn from(value: AIModelPB) -> Self { - AIModel { - name: value.name, - is_local: value.is_local, - desc: value.desc, - } - } + pub models: String, } impl From for ChatMessageListPB { @@ -314,7 +245,7 @@ impl From for ChatMessagePB { author_type: chat_message.author.author_type as i64, author_id: chat_message.author.author_id.to_string(), reply_message_id: None, - metadata: Some(serde_json::to_string(&chat_message.metadata).unwrap_or_default()), + metadata: Some(serde_json::to_string(&chat_message.meta_data).unwrap_or_default()), } } } @@ -378,6 +309,24 @@ impl From for RepeatedRelatedQuestionPB { } } +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct LLMModelInfoPB { + #[pb(index = 1)] + pub selected_model: LLMModelPB, + + #[pb(index = 2)] + pub models: Vec, +} + +impl From for LLMModelInfoPB { + fn from(value: LLMModelInfo) -> Self { + LLMModelInfoPB { + selected_model: LLMModelPB::from(value.selected_model), + models: value.models.into_iter().map(LLMModelPB::from).collect(), + } + } +} + #[derive(Debug, Clone, Default, ProtoBuf)] pub struct LLMModelPB { #[pb(index = 1)] @@ -427,12 +376,6 @@ pub struct CompleteTextPB { #[pb(index = 6)] pub rag_ids: Vec, - - #[pb(index = 7)] - pub history: Vec, - - #[pb(index = 8, one_of)] - pub custom_prompt: Option, } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -451,29 +394,6 @@ pub enum CompletionTypePB { ImproveWriting = 4, MakeShorter = 5, MakeLonger = 6, - CustomPrompt = 7, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct CompletionRecordPB { - #[pb(index = 1)] - pub role: ChatMessageTypePB, - - #[pb(index = 2)] - pub content: String, -} - -impl From<&CompletionRecordPB> for CompletionMessage { - fn from(value: &CompletionRecordPB) -> Self { - CompletionMessage { - role: match value.role { - // Coerce ChatMessageTypePB::System to AI - ChatMessageTypePB::System => "ai".to_string(), - ChatMessageTypePB::User => "human".to_string(), - }, - content: value.content.clone(), - } - } } #[derive(Default, ProtoBuf, Clone, Debug)] @@ -503,6 +423,17 @@ pub struct ChatFilePB { pub chat_id: String, } +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct DownloadLLMPB { + #[pb(index = 1)] + pub progress_stream: i64, +} + +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct DownloadTaskPB { + #[pb(index = 1)] + pub task_id: String, +} #[derive(Default, ProtoBuf, Clone, Debug)] pub struct LocalModelStatePB { #[pb(index = 1)] @@ -521,6 +452,18 @@ pub struct LocalModelStatePB { pub is_downloading: bool, } +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalModelResourcePB { + #[pb(index = 1)] + pub is_ready: bool, + + #[pb(index = 2)] + pub pending_resources: Vec, + + #[pb(index = 3)] + pub is_downloading: bool, +} + #[derive(Default, ProtoBuf, Clone, Debug)] pub struct PendingResourcePB { #[pb(index = 1)] @@ -539,33 +482,40 @@ pub struct PendingResourcePB { #[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] pub enum PendingResourceTypePB { #[default] - LocalAIAppRes = 0, - ModelRes = 1, + OfflineApp = 0, + AIModel = 1, } impl From for PendingResourceTypePB { fn from(value: PendingResource) -> Self { match value { - PendingResource::PluginExecutableNotReady { .. } => PendingResourceTypePB::LocalAIAppRes, - _ => PendingResourceTypePB::ModelRes, + PendingResource::OfflineApp { .. } => PendingResourceTypePB::OfflineApp, + PendingResource::ModelInfoRes { .. } => PendingResourceTypePB::AIModel, } } } +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalAIPluginStatePB { + #[pb(index = 1)] + pub state: RunningStatePB, + + #[pb(index = 2)] + pub offline_ai_ready: bool, +} + #[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] pub enum RunningStatePB { #[default] - ReadyToRun = 0, - Connecting = 1, - Connected = 2, - Running = 3, - Stopped = 4, + Connecting = 0, + Connected = 1, + Running = 2, + Stopped = 3, } impl From for RunningStatePB { fn from(value: RunningState) -> Self { match value { - RunningState::ReadyToConnect => RunningStatePB::ReadyToRun, RunningState::Connecting => RunningStatePB::Connecting, RunningState::Connected { .. } => RunningStatePB::Connected, RunningState::Running { .. } => RunningStatePB::Running, @@ -579,18 +529,30 @@ impl From for RunningStatePB { pub struct LocalAIPB { #[pb(index = 1)] pub enabled: bool, +} - #[pb(index = 2, one_of)] - pub lack_of_resource: Option, +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalAIChatPB { + #[pb(index = 1)] + pub enabled: bool, + + #[pb(index = 2)] + pub file_enabled: bool, #[pb(index = 3)] - pub state: RunningStatePB, + pub plugin_state: LocalAIPluginStatePB, +} - #[pb(index = 4, one_of)] - pub plugin_version: Option, +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct LocalModelStoragePB { + #[pb(index = 1)] + pub file_path: String, +} - #[pb(index = 5)] - pub plugin_downloaded: bool, +#[derive(Default, ProtoBuf, Clone, Debug)] +pub struct OfflineAIPB { + #[pb(index = 1)] + pub link: String, } #[derive(Default, ProtoBuf, Validate, Clone, Debug)] @@ -625,9 +587,6 @@ pub struct UpdateChatSettingsPB { #[pb(index = 2)] pub rag_ids: Vec, - - #[pb(index = 3)] - pub chat_model: String, } #[derive(Debug, Default, Clone, ProtoBuf)] @@ -677,74 +636,3 @@ impl From for ResponseFormat { } } } - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct LocalAISettingPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub server_url: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_model_name: String, - - #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] - pub embedding_model_name: String, -} - -impl From for LocalAISettingPB { - fn from(value: LocalAISetting) -> Self { - LocalAISettingPB { - server_url: value.ollama_server_url, - chat_model_name: value.chat_model_name, - embedding_model_name: value.embedding_model_name, - } - } -} - -impl From for LocalAISetting { - fn from(value: LocalAISettingPB) -> Self { - LocalAISetting { - ollama_server_url: value.server_url, - chat_model_name: value.chat_model_name, - embedding_model_name: value.embedding_model_name, - } - } -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LackOfAIResourcePB { - #[pb(index = 1)] - pub resource_type: LackOfAIResourceTypePB, - - #[pb(index = 2)] - pub missing_model_names: Vec, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum)] -pub enum LackOfAIResourceTypePB { - #[default] - PluginExecutableNotReady = 0, - OllamaServerNotReady = 1, - MissingModel = 2, -} - -impl From for LackOfAIResourcePB { - fn from(value: PendingResource) -> Self { - match value { - PendingResource::PluginExecutableNotReady => Self { - resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady, - missing_model_names: vec![], - }, - PendingResource::OllamaServerNotReady => Self { - resource_type: LackOfAIResourceTypePB::OllamaServerNotReady, - missing_model_names: vec![], - }, - PendingResource::MissingModel(model_name) => Self { - resource_type: LackOfAIResourceTypePB::MissingModel, - missing_model_names: vec![model_name], - }, - } - } -} diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index f85858b1c2..746a843da5 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -1,15 +1,21 @@ -use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; -use crate::completion::AICompletion; -use crate::entities::*; -use crate::util::ai_available_models_key; -use flowy_ai_pub::cloud::{AIModel, ChatMessageType}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use std::fs; use std::path::PathBuf; -use std::str::FromStr; + +use crate::ai_manager::AIManager; +use crate::completion::AICompletion; +use crate::entities::*; +use crate::local_ai::local_llm_chat::LLMModelInfo; +use crate::notification::{ + chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, +}; +use allo_isolate::Isolate; +use flowy_ai_pub::cloud::{ChatMessageMetadata, ChatMessageType, ChatRAGData, ContextLoader}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_infra::isolate_stream::IsolateSink; +use serde_json::json; use std::sync::{Arc, Weak}; -use uuid::Uuid; +use tracing::trace; use validator::Validate; fn upgrade_ai_manager(ai_manager: AFPluginState>) -> FlowyResult> { @@ -34,6 +40,7 @@ pub(crate) async fn stream_chat_message_handler( answer_stream_port, question_stream_port, format, + metadata, } = data; let message_type = match message_type { @@ -41,18 +48,44 @@ pub(crate) async fn stream_chat_message_handler( ChatMessageTypePB::User => ChatMessageType::User, }; - let chat_id = Uuid::from_str(&chat_id)?; + let metadata = metadata + .into_iter() + .map(|metadata| { + let (content_type, content_len) = match metadata.loader_type { + ContextLoaderTypePB::Txt => (ContextLoader::Text, metadata.data.len()), + ContextLoaderTypePB::Markdown => (ContextLoader::Markdown, metadata.data.len()), + ContextLoaderTypePB::PDF => (ContextLoader::PDF, 0), + ContextLoaderTypePB::UnknownLoaderType => (ContextLoader::Unknown, 0), + }; + + ChatMessageMetadata { + data: ChatRAGData { + content: metadata.data, + content_type, + size: content_len as i64, + }, + id: metadata.id, + name: metadata.name.clone(), + source: metadata.source, + extra: None, + } + }) + .collect::>(); + + trace!("Stream chat message with metadata: {:?}", metadata); + let params = StreamMessageParams { - chat_id, - message, + chat_id: &chat_id, + message: &message, message_type, answer_stream_port, question_stream_port, format, + metadata, }; let ai_manager = upgrade_ai_manager(ai_manager)?; - let result = ai_manager.stream_chat_message(params).await?; + let result = ai_manager.stream_chat_message(¶ms).await?; data_result_ok(result) } @@ -62,52 +95,33 @@ pub(crate) async fn regenerate_response_handler( ai_manager: AFPluginState>, ) -> FlowyResult<()> { let data = data.try_into_inner()?; - let chat_id = Uuid::from_str(&data.chat_id)?; let ai_manager = upgrade_ai_manager(ai_manager)?; ai_manager .stream_regenerate_response( - &chat_id, + &data.chat_id, data.answer_message_id, data.answer_stream_port, data.format, - data.model, ) .await?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_server_model_list_handler( +pub(crate) async fn get_available_model_list_handler( ai_manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - let models = ai_manager.get_available_models(source_key).await?; - data_result_ok(models) -} + let available_models = ai_manager.get_available_models().await?; + let models = available_models + .models + .into_iter() + .map(|m| m.name) + .collect::>(); -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_chat_models_handler( - data: AFPluginData, - ai_manager: AFPluginState>, -) -> DataResult { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - let models = ai_manager.get_available_models(data.source).await?; - data_result_ok(models) -} - -pub(crate) async fn update_selected_model_handler( - data: AFPluginData, - ai_manager: AFPluginState>, -) -> Result<(), FlowyError> { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager - .update_selected_model(data.source, AIModel::from(data.selected_model)) - .await?; - Ok(()) + let models = serde_json::to_string(&json!({"models": models}))?; + data_result_ok(ModelConfigPB { models }) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -119,9 +133,8 @@ pub(crate) async fn load_prev_message_handler( let data = data.into_inner(); data.validate()?; - let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_prev_chat_messages(&chat_id, data.limit as u64, data.before_message_id) + .load_prev_chat_messages(&data.chat_id, data.limit, data.before_message_id) .await?; data_result_ok(messages) } @@ -135,9 +148,8 @@ pub(crate) async fn load_next_message_handler( let data = data.into_inner(); data.validate()?; - let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .load_latest_chat_messages(&chat_id, data.limit as u64, data.after_message_id) + .load_latest_chat_messages(&data.chat_id, data.limit, data.after_message_id) .await?; data_result_ok(messages) } @@ -149,9 +161,8 @@ pub(crate) async fn get_related_question_handler( ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; let data = data.into_inner(); - let chat_id = Uuid::from_str(&data.chat_id)?; let messages = ai_manager - .get_related_questions(&chat_id, data.message_id) + .get_related_questions(&data.chat_id, data.message_id) .await?; data_result_ok(messages) } @@ -163,9 +174,8 @@ pub(crate) async fn get_answer_handler( ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; let data = data.into_inner(); - let chat_id = Uuid::from_str(&data.chat_id)?; let message = ai_manager - .generate_answer(&chat_id, data.message_id) + .generate_answer(&data.chat_id, data.message_id) .await?; data_result_ok(message) } @@ -179,20 +189,56 @@ pub(crate) async fn stop_stream_handler( data.validate()?; let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(&data.chat_id)?; - ai_manager.stop_stream(&chat_id).await?; + ai_manager.stop_stream(&data.chat_id).await?; Ok(()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn refresh_local_ai_info_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let model_info = ai_manager.local_ai_controller.refresh_model_info().await; + if model_info.is_err() { + if let Some(llm_model) = ai_manager.local_ai_controller.get_current_model() { + let model_info = LLMModelInfo { + selected_model: llm_model.clone(), + models: vec![llm_model], + }; + return data_result_ok(model_info.into()); + } + } + data_result_ok(model_info?.into()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn update_local_llm_model_handler( + data: AFPluginData, + ai_manager: AFPluginState>, +) -> DataResult { + let data = data.into_inner(); + let ai_manager = upgrade_ai_manager(ai_manager)?; + let state = ai_manager + .local_ai_controller + .select_local_llm(data.llm_id) + .await?; + data_result_ok(state) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_local_llm_state_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let state = ai_manager.local_ai_controller.get_local_llm_state().await?; + data_result_ok(state) +} + pub(crate) async fn start_complete_text_handler( data: AFPluginData, - ai_manager: AFPluginState>, tools: AFPluginState>, ) -> DataResult { - let data = data.into_inner(); - let ai_manager = upgrade_ai_manager(ai_manager)?; - let ai_model = ai_manager.get_active_model(&data.object_id).await; - let task = tools.create_complete_task(data, ai_model).await?; + let task = tools.create_complete_task(data.into_inner()).await?; data_result_ok(task) } @@ -250,17 +296,113 @@ pub(crate) async fn chat_file_handler( tracing::debug!("File size: {} bytes", file_size); let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(&data.chat_id)?; - ai_manager.chat_with_file(&chat_id, file_path).await?; + ai_manager.chat_with_file(&data.chat_id, file_path).await?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn restart_local_ai_handler( +pub(crate) async fn download_llm_resource_handler( + data: AFPluginData, + ai_manager: AFPluginState>, +) -> DataResult { + let data = data.into_inner(); + let ai_manager = upgrade_ai_manager(ai_manager)?; + let text_sink = IsolateSink::new(Isolate::new(data.progress_stream)); + let task_id = ai_manager + .local_ai_controller + .start_downloading(text_sink) + .await?; + data_result_ok(DownloadTaskPB { task_id }) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn cancel_download_llm_resource_handler( ai_manager: AFPluginState>, ) -> Result<(), FlowyError> { let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.local_ai.restart_plugin().await; + ai_manager.local_ai_controller.cancel_download()?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_plugin_state_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let state = ai_manager.local_ai_controller.get_chat_plugin_state(); + data_result_ok(state) +} +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn toggle_local_ai_chat_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let enabled = ai_manager + .local_ai_controller + .toggle_local_ai_chat() + .await?; + let file_enabled = ai_manager.local_ai_controller.is_rag_enabled(); + let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); + let pb = LocalAIChatPB { + enabled, + file_enabled, + plugin_state, + }; + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalChatAI, + ) + .payload(pb.clone()) + .send(); + data_result_ok(pb) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn toggle_local_ai_chat_file_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let enabled = ai_manager.local_ai_controller.is_chat_enabled(); + let file_enabled = ai_manager + .local_ai_controller + .toggle_local_ai_chat_rag() + .await?; + let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); + let pb = LocalAIChatPB { + enabled, + file_enabled, + plugin_state, + }; + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateLocalChatAI, + ) + .payload(pb.clone()) + .send(); + + data_result_ok(pb) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_local_ai_chat_state_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let enabled = ai_manager.local_ai_controller.is_chat_enabled(); + let file_enabled = ai_manager.local_ai_controller.is_rag_enabled(); + let plugin_state = ai_manager.local_ai_controller.get_chat_plugin_state(); + data_result_ok(LocalAIChatPB { + enabled, + file_enabled, + plugin_state, + }) +} +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn restart_local_ai_chat_handler( + ai_manager: AFPluginState>, +) -> Result<(), FlowyError> { + let ai_manager = upgrade_ai_manager(ai_manager)?; + ai_manager.local_ai_controller.restart_chat_plugin(); Ok(()) } @@ -269,9 +411,8 @@ pub(crate) async fn toggle_local_ai_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.toggle_local_ai().await?; - let state = ai_manager.local_ai.get_local_ai_state().await; - data_result_ok(state) + let enabled = ai_manager.local_ai_controller.toggle_local_ai().await?; + data_result_ok(LocalAIPB { enabled }) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -279,8 +420,31 @@ pub(crate) async fn get_local_ai_state_handler( ai_manager: AFPluginState>, ) -> DataResult { let ai_manager = upgrade_ai_manager(ai_manager)?; - let state = ai_manager.local_ai.get_local_ai_state().await; - data_result_ok(state) + let enabled = ai_manager.local_ai_controller.is_enabled(); + data_result_ok(LocalAIPB { enabled }) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_model_storage_directory_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let file_path = ai_manager + .local_ai_controller + .get_model_storage_directory()?; + data_result_ok(LocalModelStoragePB { file_path }) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_offline_app_handler( + ai_manager: AFPluginState>, +) -> DataResult { + let ai_manager = upgrade_ai_manager(ai_manager)?; + let link = ai_manager + .local_ai_controller + .get_offline_ai_app_download_link() + .await?; + data_result_ok(OfflineAIPB { link }) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -310,7 +474,6 @@ pub(crate) async fn get_chat_settings_handler( ai_manager: AFPluginState>, ) -> DataResult { let chat_id = data.try_into_inner()?.value; - let chat_id = Uuid::from_str(&chat_id)?; let ai_manager = upgrade_ai_manager(ai_manager)?; let rag_ids = ai_manager.get_rag_ids(&chat_id).await?; let pb = ChatSettingsPB { rag_ids }; @@ -324,29 +487,9 @@ pub(crate) async fn update_chat_settings_handler( ) -> FlowyResult<()> { let params = data.try_into_inner()?; let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(¶ms.chat_id.value)?; - ai_manager.update_rag_ids(&chat_id, params.rag_ids).await?; + ai_manager + .update_rag_ids(¶ms.chat_id.value, params.rag_ids) + .await?; Ok(()) } - -#[tracing::instrument(level = "debug", skip_all)] -pub(crate) async fn get_local_ai_setting_handler( - ai_manager: AFPluginState>, -) -> DataResult { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let setting = ai_manager.local_ai.get_local_ai_setting(); - let pb = LocalAISettingPB::from(setting); - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_local_ai_setting_handler( - ai_manager: AFPluginState>, - data: AFPluginData, -) -> Result<(), FlowyError> { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.update_local_ai_setting(data.into()).await?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs index 5020836a30..2bd2bee863 100644 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ b/frontend/rust-lib/flowy-ai/src/event_map.rs @@ -10,9 +10,8 @@ use crate::ai_manager::AIManager; use crate::event_handler::*; pub fn init(ai_manager: Weak) -> AFPlugin { - let strong_ai_manager = ai_manager.upgrade().unwrap(); - let user_service = Arc::downgrade(&strong_ai_manager.user_service); - let cloud_service = Arc::downgrade(&strong_ai_manager.cloud_service_wm); + let user_service = Arc::downgrade(&ai_manager.upgrade().unwrap().user_service); + let cloud_service = Arc::downgrade(&ai_manager.upgrade().unwrap().cloud_service_wm); let ai_tools = Arc::new(AICompletion::new(cloud_service, user_service)); AFPlugin::new() .name("flowy-ai") @@ -24,28 +23,47 @@ pub fn init(ai_manager: Weak) -> AFPlugin { .event(AIEvent::GetRelatedQuestion, get_related_question_handler) .event(AIEvent::GetAnswerForQuestion, get_answer_handler) .event(AIEvent::StopStream, stop_stream_handler) + .event( + AIEvent::RefreshLocalAIModelInfo, + refresh_local_ai_info_handler, + ) + .event(AIEvent::UpdateLocalLLM, update_local_llm_model_handler) + .event(AIEvent::GetLocalLLMState, get_local_llm_state_handler) .event(AIEvent::CompleteText, start_complete_text_handler) .event(AIEvent::StopCompleteText, stop_complete_text_handler) .event(AIEvent::ChatWithFile, chat_file_handler) - .event(AIEvent::RestartLocalAI, restart_local_ai_handler) + .event(AIEvent::DownloadLLMResource, download_llm_resource_handler) + .event( + AIEvent::CancelDownloadLLMResource, + cancel_download_llm_resource_handler, + ) + .event(AIEvent::GetLocalAIPluginState, get_plugin_state_handler) + .event(AIEvent::ToggleLocalAIChat, toggle_local_ai_chat_handler) + .event( + AIEvent::GetLocalAIChatState, + get_local_ai_chat_state_handler, + ) + .event(AIEvent::RestartLocalAIChat, restart_local_ai_chat_handler) .event(AIEvent::ToggleLocalAI, toggle_local_ai_handler) .event(AIEvent::GetLocalAIState, get_local_ai_state_handler) - .event(AIEvent::GetLocalAISetting, get_local_ai_setting_handler) .event( - AIEvent::UpdateLocalAISetting, - update_local_ai_setting_handler, + AIEvent::ToggleChatWithFile, + toggle_local_ai_chat_file_handler, ) .event( - AIEvent::GetServerAvailableModels, - get_server_model_list_handler, + AIEvent::GetModelStorageDirectory, + get_model_storage_directory_handler, ) + .event(AIEvent::GetOfflineAIAppLink, get_offline_app_handler) .event(AIEvent::CreateChatContext, create_chat_context_handler) .event(AIEvent::GetChatInfo, create_chat_context_handler) .event(AIEvent::GetChatSettings, get_chat_settings_handler) .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) .event(AIEvent::RegenerateResponse, regenerate_response_handler) - .event(AIEvent::GetAvailableModels, get_chat_models_handler) - .event(AIEvent::UpdateSelectedModel, update_selected_model_handler) + .event( + AIEvent::GetAvailableModels, + get_available_model_list_handler, + ) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -70,6 +88,15 @@ pub enum AIEvent { #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] GetAnswerForQuestion = 5, + #[event(input = "LLMModelPB", output = "LocalModelResourcePB")] + UpdateLocalLLM = 6, + + #[event(output = "LocalModelResourcePB")] + GetLocalLLMState = 7, + + #[event(output = "LLMModelInfoPB")] + RefreshLocalAIModelInfo = 8, + #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] CompleteText = 9, @@ -79,10 +106,26 @@ pub enum AIEvent { #[event(input = "ChatFilePB")] ChatWithFile = 11, + #[event(input = "DownloadLLMPB", output = "DownloadTaskPB")] + DownloadLLMResource = 12, + + #[event()] + CancelDownloadLLMResource = 13, + + #[event(output = "LocalAIPluginStatePB")] + GetLocalAIPluginState = 14, + + #[event(output = "LocalAIChatPB")] + ToggleLocalAIChat = 15, + + /// Return Local AI Chat State + #[event(output = "LocalAIChatPB")] + GetLocalAIChatState = 16, + /// Restart local AI chat. When plugin quit or user terminate in task manager or activity monitor, /// the plugin will need to restart. #[event()] - RestartLocalAI = 17, + RestartLocalAIChat = 17, /// Enable or disable local AI #[event(output = "LocalAIPB")] @@ -92,6 +135,15 @@ pub enum AIEvent { #[event(output = "LocalAIPB")] GetLocalAIState = 19, + #[event()] + ToggleChatWithFile = 20, + + #[event(output = "LocalModelStoragePB")] + GetModelStorageDirectory = 21, + + #[event(output = "OfflineAIPB")] + GetOfflineAIAppLink = 22, + #[event(input = "CreateChatContextPB")] CreateChatContext = 23, @@ -107,18 +159,6 @@ pub enum AIEvent { #[event(input = "RegenerateResponsePB")] RegenerateResponse = 27, - #[event(output = "AvailableModelsPB")] - GetServerAvailableModels = 28, - - #[event(output = "LocalAISettingPB")] - GetLocalAISetting = 29, - - #[event(input = "LocalAISettingPB")] - UpdateLocalAISetting = 30, - - #[event(input = "AvailableModelsQueryPB", output = "AvailableModelsPB")] - GetAvailableModels = 31, - - #[event(input = "UpdateSelectedModelPB")] - UpdateSelectedModel = 32, + #[event(output = "ModelConfigPB")] + GetAvailableModels = 28, } diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 5b582b2577..be6c743d86 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -5,14 +5,9 @@ pub mod ai_manager; mod chat; mod completion; pub mod entities; -pub mod local_ai; - -// #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -// pub mod mcp; - +mod local_ai; mod middleware; pub mod notification; -pub mod offline; +mod persistence; mod protobuf; mod stream_message; -mod util; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs deleted file mode 100644 index 1ec08854e0..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ /dev/null @@ -1,622 +0,0 @@ -use crate::entities::{LocalAIPB, RunningStatePB}; -use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use af_plugin::manager::PluginManager; -use anyhow::Error; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; -use futures::Sink; -use lib_infra::async_trait::async_trait; -use std::collections::HashMap; - -use crate::stream_message::StreamMessage; -use af_local_ai::ollama_plugin::OllamaAIPlugin; -use af_plugin::core::path::is_plugin_ready; -use af_plugin::core::plugin::RunningState; -use arc_swap::ArcSwapOption; -use flowy_ai_pub::user_service::AIUserService; -use futures_util::SinkExt; -use lib_infra::util::get_operating_system; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use tokio::select; -use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, warn}; -use uuid::Uuid; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LocalAISetting { - pub ollama_server_url: String, - pub chat_model_name: String, - pub embedding_model_name: String, -} - -impl Default for LocalAISetting { - fn default() -> Self { - Self { - ollama_server_url: "http://localhost:11434".to_string(), - chat_model_name: "llama3.1".to_string(), - embedding_model_name: "nomic-embed-text".to_string(), - } - } -} - -const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1"; - -pub struct LocalAIController { - ai_plugin: Arc, - resource: Arc, - current_chat_id: ArcSwapOption, - store_preferences: Weak, - user_service: Arc, -} - -impl Deref for LocalAIController { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.ai_plugin - } -} - -impl LocalAIController { - pub fn new( - plugin_manager: Arc, - store_preferences: Weak, - user_service: Arc, - ) -> Self { - debug!( - "[AI Plugin] init local ai controller, thread: {:?}", - std::thread::current().id() - ); - - // Create the core plugin and resource controller - let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); - let res_impl = LLMResourceServiceImpl { - store_preferences: store_preferences.clone(), - }; - let local_ai_resource = Arc::new(LocalAIResourceController::new( - user_service.clone(), - res_impl, - )); - // Subscribe to state changes - let mut running_state_rx = local_ai.subscribe_running_state(); - - let cloned_llm_res = Arc::clone(&local_ai_resource); - let cloned_store_preferences = store_preferences.clone(); - let cloned_local_ai = Arc::clone(&local_ai); - let cloned_user_service = Arc::clone(&user_service); - - // Spawn a background task to listen for plugin state changes - tokio::spawn(async move { - while let Some(state) = running_state_rx.next().await { - // Skip if we can’t get workspace_id - let Ok(workspace_id) = cloned_user_service.workspace_id() else { - continue; - }; - - let key = local_ai_enabled_key(&workspace_id.to_string()); - info!("[AI Plugin] state: {:?}", state); - - // Read whether plugin is enabled from store; default to true - if let Some(store_preferences) = cloned_store_preferences.upgrade() { - let enabled = store_preferences.get_bool(&key).unwrap_or(true); - // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled - let (plugin_downloaded, lack_of_resource) = - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - // Possibly check plugin readiness and resource concurrency in parallel, - // but here we do it sequentially for clarity. - let downloaded = is_plugin_ready(); - let resource_lack = cloned_llm_res.get_lack_of_resource().await; - (downloaded, resource_lack) - } else { - (false, None) - }; - - // If plugin is running, retrieve version - let plugin_version = if matches!(state, RunningState::Running { .. }) { - match cloned_local_ai.plugin_info().await { - Ok(info) => Some(info.version), - Err(_) => None, - } - } else { - None - }; - - // Broadcast the new local AI state - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version, - }) - .send(); - } else { - warn!("[AI Plugin] store preferences is dropped"); - } - } - }); - - Self { - ai_plugin: local_ai, - resource: local_ai_resource, - current_chat_id: ArcSwapOption::default(), - store_preferences, - user_service, - } - } - #[instrument(level = "debug", skip_all)] - pub async fn observe_plugin_resource(&self) { - let sys = get_operating_system(); - if !sys.is_desktop() { - return; - } - - debug!( - "[AI Plugin] observer plugin state. thread: {:?}", - std::thread::current().id() - ); - async fn try_init_plugin( - resource: &Arc, - ai_plugin: &Arc, - ) { - if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None).await { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } - - // Clone what is needed for the background task. - let resource_clone = self.resource.clone(); - let ai_plugin_clone = self.ai_plugin.clone(); - let mut resource_notify = self.resource.subscribe_resource_notify(); - let mut app_state_watcher = self.resource.subscribe_app_state(); - tokio::spawn(async move { - loop { - select! { - _ = app_state_watcher.recv() => { - info!("[AI Plugin] app state changed, try to init plugin"); - try_init_plugin(&resource_clone, &ai_plugin_clone).await; - }, - _ = resource_notify.recv() => { - info!("[AI Plugin] resource changed, try to init plugin"); - try_init_plugin(&resource_clone, &ai_plugin_clone).await; - }, - else => break, - } - } - }); - } - - fn upgrade_store_preferences(&self) -> FlowyResult> { - self - .store_preferences - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) - } - - /// Indicate whether the local AI plugin is running. - pub fn is_running(&self) -> bool { - self.ai_plugin.get_plugin_running_state().is_running() - } - - /// Indicate whether the local AI is enabled. - /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have - /// different settings. - pub fn is_enabled(&self) -> bool { - if !get_operating_system().is_desktop() { - return false; - } - - if let Ok(workspace_id) = self.user_service.workspace_id() { - self.is_enabled_on_workspace(&workspace_id.to_string()) - } else { - false - } - } - - pub fn is_enabled_on_workspace(&self, workspace_id: &str) -> bool { - let key = local_ai_enabled_key(workspace_id); - if !get_operating_system().is_desktop() { - return false; - } - - match self.upgrade_store_preferences() { - Ok(store) => store.get_bool(&key).unwrap_or(false), - Err(_) => false, - } - } - - pub fn get_plugin_chat_model(&self) -> Option { - if !self.is_enabled() { - return None; - } - Some(self.resource.get_llm_setting().chat_model_name) - } - - pub fn open_chat(&self, chat_id: &Uuid) { - if !self.is_enabled() { - return; - } - - // Only keep one chat open at a time. Since loading multiple models at the same time will cause - // memory issues. - if let Some(current_chat_id) = self.current_chat_id.load().as_ref() { - debug!("[AI Plugin] close previous chat: {}", current_chat_id); - self.close_chat(current_chat_id); - } - - self.current_chat_id.store(Some(Arc::new(*chat_id))); - let chat_id = chat_id.to_string(); - let weak_ctrl = Arc::downgrade(&self.ai_plugin); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.create_chat(&chat_id).await { - error!("[AI Plugin] failed to open chat: {:?}", err); - } - } - }); - } - - pub fn close_chat(&self, chat_id: &Uuid) { - if !self.is_running() { - return; - } - info!("[AI Plugin] notify close chat: {}", chat_id); - let weak_ctrl = Arc::downgrade(&self.ai_plugin); - let chat_id = chat_id.to_string(); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.close_chat(&chat_id).await { - error!("[AI Plugin] failed to close chat: {:?}", err); - } - } - }); - } - - pub fn get_local_ai_setting(&self) -> LocalAISetting { - self.resource.get_llm_setting() - } - - pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - info!( - "[AI Plugin] update local ai setting: {:?}, thread: {:?}", - setting, - std::thread::current().id() - ); - - if self.resource.set_llm_setting(setting).await.is_ok() { - let is_enabled = self.is_enabled(); - self.toggle_plugin(is_enabled).await?; - } - Ok(()) - } - - #[instrument(level = "debug", skip_all)] - pub async fn get_local_ai_state(&self) -> LocalAIPB { - let start = std::time::Instant::now(); - let enabled = self.is_enabled(); - - // If not enabled, return immediately. - if !enabled { - debug!( - "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", - start.elapsed(), - std::thread::current().id() - ); - return LocalAIPB { - enabled, - plugin_downloaded: false, - state: RunningStatePB::from(RunningState::ReadyToConnect), - lack_of_resource: None, - plugin_version: None, - }; - } - - let plugin_downloaded = is_plugin_ready(); - let state = self.ai_plugin.get_plugin_running_state(); - - // If the plugin is running, run both requests in parallel. - // Otherwise, only fetch the resource info. - let (plugin_version, lack_of_resource) = if matches!(state, RunningState::Running { .. }) { - // Launch both futures at once - let plugin_info_fut = self.ai_plugin.plugin_info(); - let resource_fut = self.resource.get_lack_of_resource(); - - let (plugin_info_res, resource_res) = tokio::join!(plugin_info_fut, resource_fut); - let plugin_version = plugin_info_res.ok().map(|info| info.version); - (plugin_version, resource_res) - } else { - let resource_res = self.resource.get_lack_of_resource().await; - (None, resource_res) - }; - - let elapsed = start.elapsed(); - debug!( - "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", - elapsed, - std::thread::current().id() - ); - - LocalAIPB { - enabled, - plugin_downloaded, - state: RunningStatePB::from(state), - lack_of_resource, - plugin_version, - } - } - #[instrument(level = "debug", skip_all)] - pub async fn restart_plugin(&self) { - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } - - pub fn get_model_storage_directory(&self) -> FlowyResult { - self - .resource - .user_model_folder() - .map(|path| path.to_string_lossy().to_string()) - } - - pub async fn toggle_local_ai(&self) -> FlowyResult { - let workspace_id = self.user_service.workspace_id()?; - let key = local_ai_enabled_key(&workspace_id.to_string()); - let store_preferences = self.upgrade_store_preferences()?; - let enabled = !store_preferences.get_bool(&key).unwrap_or(false); - tracing::trace!("[AI Plugin] toggle local ai, enabled: {}", enabled,); - store_preferences.set_bool(&key, enabled)?; - self.toggle_plugin(enabled).await?; - Ok(enabled) - } - - // #[instrument(level = "debug", skip_all)] - // pub async fn index_message_metadata( - // &self, - // chat_id: &Uuid, - // metadata_list: &[ChatMessageMetadata], - // index_process_sink: &mut (impl Sink + Unpin), - // ) -> FlowyResult<()> { - // if !self.is_enabled() { - // info!("[AI Plugin] local ai is disabled, skip indexing"); - // return Ok(()); - // } - // - // for metadata in metadata_list { - // let mut file_metadata = HashMap::new(); - // file_metadata.insert("id".to_string(), json!(&metadata.id)); - // file_metadata.insert("name".to_string(), json!(&metadata.name)); - // file_metadata.insert("source".to_string(), json!(&metadata.source)); - // - // let file_path = Path::new(&metadata.data.content); - // if !file_path.exists() { - // return Err( - // FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), - // ); - // } - // info!( - // "[AI Plugin] embed file: {:?}, with metadata: {:?}", - // file_path, file_metadata - // ); - // - // match &metadata.data.content_type { - // ContextLoader::Unknown => { - // error!( - // "[AI Plugin] unsupported content type: {:?}", - // metadata.data.content_type - // ); - // }, - // ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { - // self - // .process_index_file( - // chat_id, - // file_path.to_path_buf(), - // &file_metadata, - // index_process_sink, - // ) - // .await?; - // }, - // } - // } - // - // Ok(()) - // } - - #[allow(dead_code)] - async fn process_index_file( - &self, - chat_id: &Uuid, - file_path: PathBuf, - index_metadata: &HashMap, - index_process_sink: &mut (impl Sink + Unpin), - ) -> Result<(), FlowyError> { - let file_name = file_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let _ = index_process_sink - .send( - StreamMessage::StartIndexFile { - file_name: file_name.clone(), - } - .to_string(), - ) - .await; - - let result = self - .ai_plugin - .embed_file( - &chat_id.to_string(), - file_path, - Some(index_metadata.clone()), - ) - .await; - match result { - Ok(_) => { - let _ = index_process_sink - .send(StreamMessage::EndIndexFile { file_name }.to_string()) - .await; - }, - Err(err) => { - let _ = index_process_sink - .send(StreamMessage::IndexFileError { file_name }.to_string()) - .await; - error!("[AI Plugin] failed to index file: {:?}", err); - }, - } - - Ok(()) - } - - #[instrument(level = "debug", skip_all)] - pub(crate) async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> { - info!( - "[AI Plugin] enable: {}, thread id: {:?}", - enabled, - std::thread::current().id() - ); - if enabled { - let (tx, rx) = tokio::sync::oneshot::channel(); - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx)).await { - error!("[AI Plugin] failed to initialize local ai: {:?}", err); - } - let _ = rx.await; - } else { - if let Err(err) = self.ai_plugin.destroy_plugin().await { - error!("[AI Plugin] failed to destroy plugin: {:?}", err); - } - - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded: true, - state: RunningStatePB::Stopped, - lack_of_resource: None, - plugin_version: None, - }) - .send(); - } - Ok(()) - } -} - -#[instrument(level = "debug", skip_all, err)] -async fn initialize_ai_plugin( - plugin: &Arc, - llm_resource: &Arc, - ret: Option>, -) -> FlowyResult<()> { - let lack_of_resource = llm_resource.get_lack_of_resource().await; - - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled: true, - plugin_downloaded: true, - state: RunningStatePB::ReadyToRun, - lack_of_resource: lack_of_resource.clone(), - plugin_version: None, - }) - .send(); - - if let Some(lack_of_resource) = lack_of_resource { - info!( - "[AI Plugin] lack of resource: {:?} to initialize plugin, thread: {:?}", - lack_of_resource, - std::thread::current().id() - ); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::LocalAIResourceUpdated, - ) - .payload(lack_of_resource) - .send(); - - return Ok(()); - } - - if let Err(err) = plugin.destroy_plugin().await { - error!( - "[AI Plugin] failed to destroy plugin when lack of resource: {:?}", - err - ); - } - - let plugin = plugin.clone(); - let cloned_llm_res = llm_resource.clone(); - tokio::task::spawn_blocking(move || { - futures::executor::block_on(async move { - match cloned_llm_res.get_plugin_config(true).await { - Ok(config) => { - info!( - "[AI Plugin] initialize plugin with config: {:?}, thread: {:?}", - config, - std::thread::current().id() - ); - - match plugin.init_plugin(config).await { - Ok(_) => {}, - Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), - } - - if let Some(ret) = ret { - let _ = ret.send(()); - } - }, - Err(err) => { - error!("[AI Plugin] failed to get plugin config: {:?}", err); - }, - }; - }) - }); - - Ok(()) -} - -pub struct LLMResourceServiceImpl { - store_preferences: Weak, -} - -impl LLMResourceServiceImpl { - fn upgrade_store_preferences(&self) -> FlowyResult> { - self - .store_preferences - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) - } -} -#[async_trait] -impl LLMResourceService for LLMResourceServiceImpl { - fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { - let store_preferences = self.upgrade_store_preferences()?; - store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; - Ok(()) - } - - fn retrieve_setting(&self) -> Option { - let store_preferences = self.upgrade_store_preferences().ok()?; - store_preferences.get_object::(LOCAL_AI_SETTING_KEY) - } -} - -const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; -fn local_ai_enabled_key(workspace_id: &str) -> String { - format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs new file mode 100644 index 0000000000..ece0bdddda --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs @@ -0,0 +1,599 @@ +use crate::ai_manager::AIUserService; +use crate::entities::{LocalAIPluginStatePB, LocalModelResourcePB, RunningStatePB}; +use crate::local_ai::local_llm_resource::{LLMResourceService, LocalAIResourceController}; +use crate::notification::{ + chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, +}; +use anyhow::Error; +use appflowy_local_ai::chat_plugin::{AIPluginConfig, AppFlowyLocalAI}; +use appflowy_plugin::manager::PluginManager; +use appflowy_plugin::util::is_apple_silicon; +use flowy_ai_pub::cloud::{ + AppFlowyOfflineAI, ChatCloudService, ChatMessageMetadata, ContextLoader, LLMModel, LocalAIConfig, + SubscriptionPlan, +}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::kv::KVStorePreferences; +use futures::Sink; +use lib_infra::async_trait::async_trait; +use std::collections::HashMap; + +use crate::stream_message::StreamMessage; +use arc_swap::ArcSwapOption; +use futures_util::SinkExt; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::select; +use tokio_stream::StreamExt; +use tracing::{debug, error, info, instrument, trace, warn}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LLMSetting { + pub app: AppFlowyOfflineAI, + pub llm_model: LLMModel, +} + +pub struct LLMModelInfo { + pub selected_model: LLMModel, + pub models: Vec, +} + +const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; +const APPFLOWY_LOCAL_AI_CHAT_ENABLED: &str = "appflowy_local_ai_chat_enabled"; +const APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED: &str = "appflowy_local_ai_chat_rag_enabled"; +const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v0"; + +pub struct LocalAIController { + local_ai: Arc, + local_ai_resource: Arc, + current_chat_id: ArcSwapOption, + store_preferences: Arc, + user_service: Arc, + cloud_service: Arc, +} + +impl Deref for LocalAIController { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.local_ai + } +} + +impl LocalAIController { + pub fn new( + plugin_manager: Arc, + store_preferences: Arc, + user_service: Arc, + cloud_service: Arc, + ) -> Self { + let local_ai = Arc::new(AppFlowyLocalAI::new(plugin_manager)); + let res_impl = LLMResourceServiceImpl { + user_service: user_service.clone(), + cloud_service: cloud_service.clone(), + store_preferences: store_preferences.clone(), + }; + + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + let llm_res = Arc::new(LocalAIResourceController::new( + user_service.clone(), + res_impl, + tx, + )); + let current_chat_id = ArcSwapOption::default(); + + let mut running_state_rx = local_ai.subscribe_running_state(); + let cloned_llm_res = llm_res.clone(); + tokio::spawn(async move { + while let Some(state) = running_state_rx.next().await { + info!("[AI Plugin] state: {:?}", state); + let offline_ai_ready = cloned_llm_res.is_offline_app_ready(); + let new_state = RunningStatePB::from(state); + chat_notification_builder( + APPFLOWY_AI_NOTIFICATION_KEY, + ChatNotification::UpdateChatPluginState, + ) + .payload(LocalAIPluginStatePB { + state: new_state, + offline_ai_ready, + }) + .send(); + } + }); + + let this = Self { + local_ai, + local_ai_resource: llm_res, + current_chat_id, + store_preferences, + user_service, + cloud_service, + }; + + let rag_enabled = this.is_rag_enabled(); + let cloned_llm_chat = this.local_ai.clone(); + let cloned_llm_res = this.local_ai_resource.clone(); + let mut offline_ai_watch = this.local_ai_resource.subscribe_offline_app_state(); + tokio::spawn(async move { + let init_fn = || { + if let Ok(chat_config) = cloned_llm_res.get_chat_config(rag_enabled) { + if let Err(err) = initialize_ai_plugin(&cloned_llm_chat, chat_config, None) { + error!("[AI Plugin] failed to setup plugin: {:?}", err); + } + } + }; + + loop { + select! { + _ = offline_ai_watch.recv() => { + init_fn(); + }, + _ = rx.recv() => { + init_fn(); + }, + else => { break; } + } + } + }); + + if this.can_init_plugin() { + let result = this + .local_ai_resource + .get_chat_config(this.is_rag_enabled()); + if let Ok(chat_config) = result { + if let Err(err) = initialize_ai_plugin(&this.local_ai, chat_config, None) { + error!("[AI Plugin] failed to setup plugin: {:?}", err); + } + } + } + + this + } + pub async fn refresh(&self) -> FlowyResult<()> { + let is_enabled = self.is_enabled(); + self.enable_chat_plugin(is_enabled).await?; + + if is_enabled { + let local_ai = self.local_ai.clone(); + let workspace_id = self.user_service.workspace_id()?; + let cloned_service = self.cloud_service.clone(); + let store_preferences = self.store_preferences.clone(); + tokio::spawn(async move { + let key = local_ai_enabled_key(&workspace_id); + match cloned_service.get_workspace_plan(&workspace_id).await { + Ok(plans) => { + trace!("[AI Plugin] workspace:{} plans: {:?}", workspace_id, plans); + if !plans.contains(&SubscriptionPlan::AiLocal) { + info!( + "disable local ai plugin for workspace: {}. reason: no plan found", + workspace_id + ); + let _ = store_preferences.set_bool(&key, false); + let _ = local_ai.destroy_chat_plugin().await; + } + }, + Err(err) => { + warn!("[AI Plugin]: failed to get workspace plan: {:?}", err); + }, + } + }); + } + + Ok(()) + } + + pub async fn refresh_model_info(&self) -> FlowyResult { + self.local_ai_resource.refresh_llm_resource().await + } + + /// Returns true if the local AI is enabled and ready to use. + pub fn can_init_plugin(&self) -> bool { + self.is_enabled() && self.local_ai_resource.is_resource_ready() + } + + /// Indicate whether the local AI plugin is running. + pub fn is_running(&self) -> bool { + if !self.is_enabled() { + return false; + } + self.local_ai.get_plugin_running_state().is_ready() + } + + /// Indicate whether the local AI is enabled. + /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have + /// different settings. + pub fn is_enabled(&self) -> bool { + if let Ok(key) = self + .user_service + .workspace_id() + .map(|workspace_id| local_ai_enabled_key(&workspace_id)) + { + self.store_preferences.get_bool(&key).unwrap_or(true) + } else { + false + } + } + + /// Indicate whether the local AI chat is enabled. In the future, we can support multiple + /// AI plugin. + pub fn is_chat_enabled(&self) -> bool { + self + .store_preferences + .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) + .unwrap_or(true) + } + + pub fn is_rag_enabled(&self) -> bool { + self + .store_preferences + .get_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED) + .unwrap_or(true) + } + + pub fn open_chat(&self, chat_id: &str) { + if !self.is_enabled() { + return; + } + + // Only keep one chat open at a time. Since loading multiple models at the same time will cause + // memory issues. + if let Some(current_chat_id) = self.current_chat_id.load().as_ref() { + debug!("[AI Plugin] close previous chat: {}", current_chat_id); + self.close_chat(current_chat_id); + } + + self + .current_chat_id + .store(Some(Arc::new(chat_id.to_string()))); + let chat_id = chat_id.to_string(); + let weak_ctrl = Arc::downgrade(&self.local_ai); + tokio::spawn(async move { + if let Some(ctrl) = weak_ctrl.upgrade() { + if let Err(err) = ctrl.create_chat(&chat_id).await { + error!("[AI Plugin] failed to open chat: {:?}", err); + } + } + }); + } + + pub fn close_chat(&self, chat_id: &str) { + if !self.is_running() { + return; + } + info!("[AI Plugin] notify close chat: {}", chat_id); + let weak_ctrl = Arc::downgrade(&self.local_ai); + let chat_id = chat_id.to_string(); + tokio::spawn(async move { + if let Some(ctrl) = weak_ctrl.upgrade() { + if let Err(err) = ctrl.close_chat(&chat_id).await { + error!("[AI Plugin] failed to close chat: {:?}", err); + } + } + }); + } + + pub async fn select_local_llm(&self, llm_id: i64) -> FlowyResult { + if !self.is_enabled() { + return Err(FlowyError::local_ai_unavailable()); + } + + if let Some(model) = self.local_ai_resource.get_selected_model() { + if model.llm_id == llm_id { + return self.local_ai_resource.get_local_llm_state(); + } + } + + let state = self.local_ai_resource.use_local_llm(llm_id)?; + // Re-initialize the plugin if the setting is updated and ready to use + if self.local_ai_resource.is_resource_ready() { + let chat_config = self + .local_ai_resource + .get_chat_config(self.is_rag_enabled())?; + if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, None) { + error!("failed to setup plugin: {:?}", err); + } + } + Ok(state) + } + + pub async fn get_local_llm_state(&self) -> FlowyResult { + self.local_ai_resource.get_local_llm_state() + } + + pub fn get_current_model(&self) -> Option { + self.local_ai_resource.get_selected_model() + } + + pub async fn start_downloading(&self, progress_sink: T) -> FlowyResult + where + T: Sink + Unpin + Sync + Send + 'static, + { + let task_id = self + .local_ai_resource + .start_downloading(progress_sink) + .await?; + Ok(task_id) + } + + pub fn cancel_download(&self) -> FlowyResult<()> { + self.local_ai_resource.cancel_download()?; + Ok(()) + } + + pub fn get_chat_plugin_state(&self) -> LocalAIPluginStatePB { + if !self.is_enabled() { + return LocalAIPluginStatePB { + state: RunningStatePB::Stopped, + offline_ai_ready: false, + }; + } + + let offline_ai_ready = self.local_ai_resource.is_offline_app_ready(); + let state = self.local_ai.get_plugin_running_state(); + LocalAIPluginStatePB { + state: RunningStatePB::from(state), + offline_ai_ready, + } + } + + pub fn restart_chat_plugin(&self) { + let rag_enabled = self.is_rag_enabled(); + if let Ok(chat_config) = self.local_ai_resource.get_chat_config(rag_enabled) { + if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, None) { + error!("[AI Plugin] failed to setup plugin: {:?}", err); + } + } + } + + pub fn get_model_storage_directory(&self) -> FlowyResult { + self + .local_ai_resource + .user_model_folder() + .map(|path| path.to_string_lossy().to_string()) + } + + pub async fn get_offline_ai_app_download_link(&self) -> FlowyResult { + self + .local_ai_resource + .get_offline_ai_app_download_link() + .await + } + + pub async fn toggle_local_ai(&self) -> FlowyResult { + let workspace_id = self.user_service.workspace_id()?; + let key = local_ai_enabled_key(&workspace_id); + let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true); + self.store_preferences.set_bool(&key, enabled)?; + + // when enable local ai. we need to check if chat is enabled, if enabled, we need to init chat plugin + // otherwise, we need to destroy the plugin + if enabled { + let chat_enabled = self + .store_preferences + .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) + .unwrap_or(true); + + if self.local_ai_resource.is_resource_ready() { + self.enable_chat_plugin(chat_enabled).await?; + } + } else { + let _ = self.enable_chat_plugin(false).await; + } + Ok(enabled) + } + + pub async fn toggle_local_ai_chat(&self) -> FlowyResult { + let enabled = !self + .store_preferences + .get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED) + .unwrap_or(true); + self + .store_preferences + .set_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED, enabled)?; + self.enable_chat_plugin(enabled).await?; + + Ok(enabled) + } + + pub async fn toggle_local_ai_chat_rag(&self) -> FlowyResult { + let enabled = !self + .store_preferences + .get_bool_or_default(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED); + self + .store_preferences + .set_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED, enabled)?; + Ok(enabled) + } + pub async fn index_message_metadata( + &self, + chat_id: &str, + metadata_list: &[ChatMessageMetadata], + index_process_sink: &mut (impl Sink + Unpin), + ) -> FlowyResult<()> { + if !self.is_enabled() { + return Ok(()); + } + + for metadata in metadata_list { + if let Err(err) = metadata.data.validate() { + error!( + "[AI Plugin] invalid metadata: {:?}, error: {:?}", + metadata, err + ); + continue; + } + + let mut index_metadata = HashMap::new(); + index_metadata.insert("id".to_string(), json!(&metadata.id)); + index_metadata.insert("name".to_string(), json!(&metadata.name)); + index_metadata.insert("at_name".to_string(), json!(format!("@{}", &metadata.name))); + index_metadata.insert("source".to_string(), json!(&metadata.source)); + match &metadata.data.content_type { + ContextLoader::Unknown => { + error!( + "[AI Plugin] unsupported content type: {:?}", + metadata.data.content_type + ); + }, + ContextLoader::Text | ContextLoader::Markdown => { + trace!("[AI Plugin]: index text: {}", metadata.data.content); + self + .process_index_file( + chat_id, + None, + Some(metadata.data.content.clone()), + metadata, + &index_metadata, + index_process_sink, + ) + .await?; + }, + ContextLoader::PDF => { + trace!("[AI Plugin]: index pdf file: {}", metadata.data.content); + let file_path = Path::new(&metadata.data.content); + if file_path.exists() { + self + .process_index_file( + chat_id, + Some(file_path.to_path_buf()), + None, + metadata, + &index_metadata, + index_process_sink, + ) + .await?; + } + }, + } + } + + Ok(()) + } + + async fn process_index_file( + &self, + chat_id: &str, + file_path: Option, + content: Option, + metadata: &ChatMessageMetadata, + index_metadata: &HashMap, + index_process_sink: &mut (impl Sink + Unpin), + ) -> Result<(), FlowyError> { + let _ = index_process_sink + .send( + StreamMessage::StartIndexFile { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + + let result = self + .index_file(chat_id, file_path, content, Some(index_metadata.clone())) + .await; + match result { + Ok(_) => { + let _ = index_process_sink + .send( + StreamMessage::EndIndexFile { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + }, + Err(err) => { + let _ = index_process_sink + .send( + StreamMessage::IndexFileError { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + error!("[AI Plugin] failed to index file: {:?}", err); + }, + } + + Ok(()) + } + + async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> { + info!("[AI Plugin] enable chat plugin: {}", enabled); + if enabled { + let (tx, rx) = tokio::sync::oneshot::channel(); + let chat_config = self + .local_ai_resource + .get_chat_config(self.is_rag_enabled())?; + if let Err(err) = initialize_ai_plugin(&self.local_ai, chat_config, Some(tx)) { + error!("[AI Plugin] failed to initialize local ai: {:?}", err); + } + let _ = rx.await; + } else if let Err(err) = self.local_ai.destroy_chat_plugin().await { + error!("[AI Plugin] failed to destroy plugin: {:?}", err); + } + Ok(()) + } +} + +#[instrument(level = "debug", skip_all, err)] +fn initialize_ai_plugin( + llm_chat: &Arc, + mut chat_config: AIPluginConfig, + ret: Option>, +) -> FlowyResult<()> { + let llm_chat = llm_chat.clone(); + + tokio::spawn(async move { + info!("[AI Plugin] config: {:?}", chat_config); + if is_apple_silicon().await.unwrap_or(false) { + chat_config = chat_config.with_device("gpu"); + } + match llm_chat.init_chat_plugin(chat_config).await { + Ok(_) => {}, + Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), + } + + if let Some(ret) = ret { + let _ = ret.send(()); + } + }); + Ok(()) +} + +pub struct LLMResourceServiceImpl { + user_service: Arc, + cloud_service: Arc, + store_preferences: Arc, +} +#[async_trait] +impl LLMResourceService for LLMResourceServiceImpl { + async fn fetch_local_ai_config(&self) -> Result { + let workspace_id = self.user_service.workspace_id()?; + let config = self + .cloud_service + .get_local_ai_config(&workspace_id) + .await?; + Ok(config) + } + + fn store_setting(&self, setting: LLMSetting) -> Result<(), Error> { + self + .store_preferences + .set_object(LOCAL_AI_SETTING_KEY, &setting)?; + Ok(()) + } + + fn retrieve_setting(&self) -> Option { + self + .store_preferences + .get_object::(LOCAL_AI_SETTING_KEY) + } +} + +fn local_ai_enabled_key(workspace_id: &str) -> String { + format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) +} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs new file mode 100644 index 0000000000..90dc328a6d --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_resource.rs @@ -0,0 +1,534 @@ +use crate::ai_manager::AIUserService; +use crate::entities::{LocalModelResourcePB, PendingResourcePB, PendingResourceTypePB}; +use crate::local_ai::local_llm_chat::{LLMModelInfo, LLMSetting}; +use crate::local_ai::model_request::download_model; + +use appflowy_local_ai::chat_plugin::AIPluginConfig; +use flowy_ai_pub::cloud::{LLMModel, LocalAIConfig, ModelInfo}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use futures::Sink; +use futures_util::SinkExt; +use lib_infra::async_trait::async_trait; + +use arc_swap::ArcSwapOption; +use lib_infra::util::{get_operating_system, OperatingSystem}; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::local_ai::watch::offline_app_path; +#[cfg(target_os = "macos")] +use crate::local_ai::watch::{watch_offline_app, WatchContext}; +use tokio::fs::{self}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, trace, warn}; + +#[async_trait] +pub trait LLMResourceService: Send + Sync + 'static { + /// Get local ai configuration from remote server + async fn fetch_local_ai_config(&self) -> Result; + fn store_setting(&self, setting: LLMSetting) -> Result<(), anyhow::Error>; + fn retrieve_setting(&self) -> Option; +} + +const LLM_MODEL_DIR: &str = "models"; +const DOWNLOAD_FINISH: &str = "finish"; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum WatchDiskEvent { + Create, + Remove, +} + +pub enum PendingResource { + OfflineApp, + ModelInfoRes(Vec), +} +#[derive(Clone)] +pub struct DownloadTask { + cancel_token: CancellationToken, + tx: tokio::sync::broadcast::Sender, +} +impl DownloadTask { + pub fn new() -> Self { + let (tx, _) = tokio::sync::broadcast::channel(100); + let cancel_token = CancellationToken::new(); + Self { cancel_token, tx } + } + + pub fn cancel(&self) { + self.cancel_token.cancel(); + } +} + +pub struct LocalAIResourceController { + user_service: Arc, + resource_service: Arc, + llm_setting: ArcSwapOption, + // The ai_config will be set when user try to get latest local ai config from server + ai_config: ArcSwapOption, + download_task: Arc>, + resource_notify: tokio::sync::mpsc::Sender<()>, + #[cfg(target_os = "macos")] + #[allow(dead_code)] + offline_app_disk_watch: Option, + offline_app_state_sender: tokio::sync::broadcast::Sender, +} + +impl LocalAIResourceController { + pub fn new( + user_service: Arc, + resource_service: impl LLMResourceService, + resource_notify: tokio::sync::mpsc::Sender<()>, + ) -> Self { + let (offline_app_state_sender, _) = tokio::sync::broadcast::channel(1); + let llm_setting = resource_service.retrieve_setting().map(Arc::new); + #[cfg(target_os = "macos")] + let mut offline_app_disk_watch: Option = None; + + #[cfg(target_os = "macos")] + { + match watch_offline_app() { + Ok((new_watcher, mut rx)) => { + let sender = offline_app_state_sender.clone(); + tokio::spawn(async move { + while let Some(event) = rx.recv().await { + if let Err(err) = sender.send(event) { + error!("[LLM Resource] Failed to send offline app state: {:?}", err); + } + } + }); + offline_app_disk_watch = Some(new_watcher); + }, + Err(err) => { + error!("[LLM Resource] Failed to watch offline app path: {:?}", err); + }, + } + } + + Self { + user_service, + resource_service: Arc::new(resource_service), + llm_setting: ArcSwapOption::new(llm_setting), + ai_config: Default::default(), + download_task: Default::default(), + resource_notify, + #[cfg(target_os = "macos")] + offline_app_disk_watch, + offline_app_state_sender, + } + } + + #[allow(dead_code)] + pub fn subscribe_offline_app_state(&self) -> tokio::sync::broadcast::Receiver { + self.offline_app_state_sender.subscribe() + } + + fn set_llm_setting(&self, llm_setting: LLMSetting) { + self.llm_setting.store(Some(llm_setting.into())); + } + + /// Returns true when all resources are downloaded and ready to use. + pub fn is_resource_ready(&self) -> bool { + match self.calculate_pending_resources() { + Ok(res) => res.is_empty(), + Err(_) => false, + } + } + + pub fn is_offline_app_ready(&self) -> bool { + offline_app_path().exists() + } + + pub async fn get_offline_ai_app_download_link(&self) -> FlowyResult { + let ai_config = self.fetch_ai_config().await?; + Ok(ai_config.plugin.url) + } + + /// Retrieves model information and updates the current model settings. + #[instrument(level = "debug", skip_all, err)] + pub async fn refresh_llm_resource(&self) -> FlowyResult { + let ai_config = self.fetch_ai_config().await?; + if ai_config.models.is_empty() { + return Err(FlowyError::local_ai().with_context("No model found")); + } + + self.ai_config.store(Some(ai_config.clone().into())); + let selected_model = self.select_model(&ai_config)?; + + let llm_setting = LLMSetting { + app: ai_config.plugin.clone(), + llm_model: selected_model.clone(), + }; + self.set_llm_setting(llm_setting.clone()); + self.resource_service.store_setting(llm_setting)?; + + Ok(LLMModelInfo { + selected_model, + models: ai_config.models, + }) + } + + #[instrument(level = "info", skip_all, err)] + pub fn use_local_llm(&self, llm_id: i64) -> FlowyResult { + let (app, llm_model) = self + .ai_config + .load() + .as_ref() + .and_then(|config| { + config + .models + .iter() + .find(|model| model.llm_id == llm_id) + .cloned() + .map(|model| (config.plugin.clone(), model)) + }) + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + + let llm_setting = LLMSetting { + app, + llm_model: llm_model.clone(), + }; + + trace!("[LLM Resource] Selected AI setting: {:?}", llm_setting); + self.set_llm_setting(llm_setting.clone()); + self.resource_service.store_setting(llm_setting)?; + self.get_local_llm_state() + } + + pub fn get_local_llm_state(&self) -> FlowyResult { + let state = self + .check_resource() + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + Ok(state) + } + + #[instrument(level = "debug", skip_all)] + fn check_resource(&self) -> Option { + trace!("[LLM Resource] Checking local ai resources"); + + let pending_resources = self.calculate_pending_resources().ok()?; + let is_ready = pending_resources.is_empty(); + let is_downloading = self.download_task.load().is_some(); + let pending_resources: Vec<_> = pending_resources + .into_iter() + .flat_map(|res| match res { + PendingResource::OfflineApp => vec![PendingResourcePB { + name: "AppFlowy Plugin".to_string(), + file_size: "0 GB".to_string(), + requirements: "".to_string(), + res_type: PendingResourceTypePB::OfflineApp, + }], + PendingResource::ModelInfoRes(model_infos) => model_infos + .into_iter() + .map(|model_info| PendingResourcePB { + name: model_info.name, + file_size: bytes_to_readable_format(model_info.file_size as u64), + requirements: model_info.requirements, + res_type: PendingResourceTypePB::AIModel, + }) + .collect::>(), + }) + .collect(); + + let resource = LocalModelResourcePB { + is_ready, + pending_resources, + is_downloading, + }; + + debug!("[LLM Resource] Local AI resources state: {:?}", resource); + Some(resource) + } + + /// Returns true when all resources are downloaded and ready to use. + pub fn calculate_pending_resources(&self) -> FlowyResult> { + match self.llm_setting.load().as_ref() { + None => Err(FlowyError::local_ai().with_context("Can't find any llm config")), + Some(llm_setting) => { + let mut resources = vec![]; + let app_path = offline_app_path(); + if !app_path.exists() { + trace!("[LLM Resource] offline app not found: {:?}", app_path); + resources.push(PendingResource::OfflineApp); + } + + let chat_model = self.model_path(&llm_setting.llm_model.chat_model.file_name)?; + if !chat_model.exists() { + resources.push(PendingResource::ModelInfoRes(vec![llm_setting + .llm_model + .chat_model + .clone()])); + } + + let embedding_model = self.model_path(&llm_setting.llm_model.embedding_model.file_name)?; + if !embedding_model.exists() { + resources.push(PendingResource::ModelInfoRes(vec![llm_setting + .llm_model + .embedding_model + .clone()])); + } + + Ok(resources) + }, + } + } + + #[instrument(level = "info", skip_all, err)] + pub async fn start_downloading(&self, mut progress_sink: T) -> FlowyResult + where + T: Sink + Unpin + Sync + Send + 'static, + { + let task_id = uuid::Uuid::new_v4().to_string(); + let weak_download_task = Arc::downgrade(&self.download_task); + let resource_notify = self.resource_notify.clone(); + // notify download progress to client. + let progress_notify = |mut rx: tokio::sync::broadcast::Receiver| { + tokio::spawn(async move { + while let Ok(value) = rx.recv().await { + let is_finish = value == DOWNLOAD_FINISH; + if let Err(err) = progress_sink.send(value).await { + warn!("Failed to send progress: {:?}", err); + break; + } + + if is_finish { + info!("notify download finish, need to reload resources"); + let _ = resource_notify.send(()).await; + if let Some(download_task) = weak_download_task.upgrade() { + if let Some(task) = download_task.swap(None) { + task.cancel(); + } + } + break; + } + } + }); + }; + + // return immediately if download task already exists + { + let guard = self.download_task.load(); + if let Some(download_task) = &*guard { + trace!( + "Download task already exists, return the task id: {}", + task_id + ); + progress_notify(download_task.tx.subscribe()); + return Ok(task_id); + } + } + + // If download task is not exists, create a new download task. + info!("[LLM Resource] Start new download task"); + let llm_setting = self + .llm_setting + .load_full() + .ok_or_else(|| FlowyError::local_ai().with_context("No local ai config found"))?; + + let download_task = Arc::new(DownloadTask::new()); + self.download_task.store(Some(download_task.clone())); + progress_notify(download_task.tx.subscribe()); + + let model_dir = self.user_model_folder()?; + if !model_dir.exists() { + fs::create_dir_all(&model_dir).await.map_err(|err| { + FlowyError::local_ai().with_context(format!("Failed to create model dir: {:?}", err)) + })?; + } + + tokio::spawn(async move { + // After download the plugin, start downloading models + let chat_model_file = ( + model_dir.join(&llm_setting.llm_model.chat_model.file_name), + &llm_setting.llm_model.chat_model.file_name, + &llm_setting.llm_model.chat_model.name, + &llm_setting.llm_model.chat_model.download_url, + ); + let embedding_model_file = ( + model_dir.join(&llm_setting.llm_model.embedding_model.file_name), + &llm_setting.llm_model.embedding_model.file_name, + &llm_setting.llm_model.embedding_model.name, + &llm_setting.llm_model.embedding_model.download_url, + ); + for (file_path, file_name, model_name, url) in [chat_model_file, embedding_model_file] { + if file_path.exists() { + continue; + } + + info!("[LLM Resource] Downloading model: {:?}", file_name); + let plugin_progress_tx = download_task.tx.clone(); + let cloned_model_name = model_name.clone(); + let progress = Arc::new(move |downloaded, total_size| { + let progress = (downloaded as f64 / total_size as f64).clamp(0.0, 1.0); + if plugin_progress_tx.receiver_count() == 0 { + return; + } + + if let Err(err) = + plugin_progress_tx.send(format!("{}:progress:{}", cloned_model_name, progress)) + { + warn!("Failed to send progress: {:?}", err); + } + }); + match download_model( + url, + &model_dir, + file_name, + Some(progress), + Some(download_task.cancel_token.clone()), + ) + .await + { + Ok(_) => info!("[LLM Resource] Downloaded model: {:?}", file_name), + Err(err) => { + error!( + "[LLM Resource] Failed to download model for given url: {:?}, error: {:?}", + url, err + ); + download_task + .tx + .send(format!("error:failed to download {}", model_name))?; + continue; + }, + } + } + info!("[LLM Resource] All resources downloaded"); + download_task.tx.send(DOWNLOAD_FINISH.to_string())?; + Ok::<_, anyhow::Error>(()) + }); + + Ok(task_id) + } + + pub fn cancel_download(&self) -> FlowyResult<()> { + if let Some(cancel_token) = self.download_task.swap(None) { + info!("[LLM Resource] Cancel download"); + cancel_token.cancel(); + } + + Ok(()) + } + + #[instrument(level = "info", skip_all)] + pub fn get_chat_config(&self, rag_enabled: bool) -> FlowyResult { + if !self.is_resource_ready() { + return Err(FlowyError::local_ai().with_context("Local AI resources are not ready")); + } + + let llm_setting = self + .llm_setting + .load_full() + .ok_or_else(|| FlowyError::local_ai().with_context("No local llm setting found"))?; + + let model_dir = self.user_model_folder()?; + let bin_path = match get_operating_system() { + OperatingSystem::MacOS => { + let path = offline_app_path(); + if !path.exists() { + return Err(FlowyError::new( + ErrorCode::AIOfflineNotInstalled, + format!("AppFlowy Offline not installed at path: {:?}", path), + )); + } + path + }, + _ => { + return Err( + FlowyError::local_ai_unavailable() + .with_context("Local AI not available on current platform"), + ); + }, + }; + + let chat_model_path = model_dir.join(&llm_setting.llm_model.chat_model.file_name); + let mut config = AIPluginConfig::new(bin_path, chat_model_path)?; + + if rag_enabled { + let resource_dir = self.resource_dir()?; + let embedding_model_path = model_dir.join(&llm_setting.llm_model.embedding_model.file_name); + let persist_directory = resource_dir.join("vectorstore"); + if !persist_directory.exists() { + std::fs::create_dir_all(&persist_directory)?; + } + config.set_rag_enabled(&embedding_model_path, &persist_directory)?; + } + + if cfg!(debug_assertions) { + config = config.with_verbose(true); + } + trace!("[AI Chat] use config: {:?}", config); + Ok(config) + } + + /// Fetches the local AI configuration from the resource service. + async fn fetch_ai_config(&self) -> FlowyResult { + self + .resource_service + .fetch_local_ai_config() + .await + .map_err(|err| { + error!("[LLM Resource] Failed to fetch local ai config: {:?}", err); + FlowyError::local_ai() + .with_context("Can't retrieve model info. Please try again later".to_string()) + }) + } + + pub fn get_selected_model(&self) -> Option { + let setting = self.llm_setting.load(); + Some(setting.as_ref()?.llm_model.clone()) + } + + /// Selects the appropriate model based on the current settings or defaults to the first model. + fn select_model(&self, ai_config: &LocalAIConfig) -> FlowyResult { + let llm_setting = self.llm_setting.load(); + let selected_model = match &*llm_setting { + None => ai_config.models[0].clone(), + Some(llm_setting) => { + match ai_config + .models + .iter() + .find(|model| model.llm_id == llm_setting.llm_model.llm_id) + { + None => ai_config.models[0].clone(), + Some(llm_model) => { + if llm_model != &llm_setting.llm_model { + info!( + "[LLM Resource] existing model is different from remote, replace with remote model" + ); + } + llm_model.clone() + }, + } + }, + }; + Ok(selected_model) + } + + pub(crate) fn user_model_folder(&self) -> FlowyResult { + self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) + } + + fn model_path(&self, model_file_name: &str) -> FlowyResult { + self + .user_model_folder() + .map(|dir| dir.join(model_file_name)) + } + + pub(crate) fn resource_dir(&self) -> FlowyResult { + let user_data_dir = self.user_service.application_root_dir()?; + Ok(user_data_dir.join("ai")) + } +} +fn bytes_to_readable_format(bytes: u64) -> String { + const BYTES_IN_GIGABYTE: u64 = 1024 * 1024 * 1024; + const BYTES_IN_MEGABYTE: u64 = 1024 * 1024; + + if bytes >= BYTES_IN_GIGABYTE { + let gigabytes = (bytes as f64) / (BYTES_IN_GIGABYTE as f64); + format!("{:.1} GB", gigabytes) + } else { + let megabytes = (bytes as f64) / (BYTES_IN_MEGABYTE as f64); + format!("{:.2} MB", megabytes) + } +} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs b/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs index c0fd967d43..5daddf881b 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs @@ -1,6 +1,6 @@ -pub mod controller; -mod request; -pub mod resource; +pub mod local_llm_chat; +pub mod local_llm_resource; +mod model_request; pub mod stream_util; pub mod watch; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs b/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs similarity index 72% rename from frontend/rust-lib/flowy-ai/src/local_ai/request.rs rename to frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs index 6d4bd3289d..c37a6f04ff 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/model_request.rs @@ -12,7 +12,6 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio_util::sync::CancellationToken; use tracing::{instrument, trace}; -#[allow(dead_code)] type ProgressCallback = Arc; #[instrument(level = "trace", skip_all, err)] @@ -96,7 +95,6 @@ pub async fn download_model( Ok(download_path) } -#[allow(dead_code)] async fn make_request( client: &Client, url: &str, @@ -116,3 +114,46 @@ async fn make_request( } Ok(response) } + +#[cfg(test)] +mod test { + use super::*; + use std::env::temp_dir; + #[tokio::test] + async fn retrieve_gpt4all_model_test() { + for url in [ + // "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", + "https://huggingface.co/second-state/All-MiniLM-L6-v2-Embedding-GGUF/resolve/main/all-MiniLM-L6-v2-Q3_K_L.gguf?download=true", + // "https://huggingface.co/MaziyarPanahi/Mistral-7B-Instruct-v0.3-GGUF/resolve/main/Mistral-7B-Instruct-v0.3.Q4_K_M.gguf?download=true", + ] { + let temp_dir = temp_dir().join("download_llm"); + if !temp_dir.exists() { + fs::create_dir(&temp_dir).await.unwrap(); + } + let file_name = "llm_model.gguf"; + let cancel_token = CancellationToken::new(); + let token = cancel_token.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(120)).await; + token.cancel(); + }); + + let download_file = download_model( + url, + &temp_dir, + file_name, + Some(Arc::new(|a, b| { + println!("{}/{}", a, b); + })), + Some(cancel_token), + ).await.unwrap(); + + let file_path = temp_dir.join(file_name); + assert_eq!(download_file, file_path); + + println!("File path: {:?}", file_path); + assert!(file_path.exists()); + std::fs::remove_file(file_path).unwrap(); + } + } +} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs deleted file mode 100644 index 36a56e171d..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::local_ai::controller::LocalAISetting; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use lib_infra::async_trait::async_trait; - -use crate::entities::LackOfAIResourcePB; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use crate::local_ai::watch::{watch_offline_app, WatchContext}; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use af_local_ai::ollama_plugin::OllamaPluginConfig; -use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path}; -use flowy_ai_pub::user_service::AIUserService; -use lib_infra::util::{get_operating_system, OperatingSystem}; -use reqwest::Client; -use serde::Deserialize; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use tracing::{error, info, instrument, trace}; - -#[derive(Debug, Deserialize)] -struct TagsResponse { - models: Vec, -} - -#[derive(Debug, Deserialize)] -struct ModelEntry { - name: String, -} - -#[async_trait] -pub trait LLMResourceService: Send + Sync + 'static { - /// Get local ai configuration from remote server - fn store_setting(&self, setting: LocalAISetting) -> Result<(), anyhow::Error>; - fn retrieve_setting(&self) -> Option; -} - -const LLM_MODEL_DIR: &str = "models"; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum WatchDiskEvent { - Create, - Remove, -} - -#[derive(Debug, Clone)] -pub enum PendingResource { - PluginExecutableNotReady, - OllamaServerNotReady, - MissingModel(String), -} - -pub struct LocalAIResourceController { - user_service: Arc, - resource_service: Arc, - resource_notify: tokio::sync::broadcast::Sender<()>, - #[cfg(any(target_os = "macos", target_os = "linux"))] - #[allow(dead_code)] - app_disk_watch: Option, - app_state_sender: tokio::sync::broadcast::Sender, -} - -impl LocalAIResourceController { - pub fn new( - user_service: Arc, - resource_service: impl LLMResourceService, - ) -> Self { - let (resource_notify, _) = tokio::sync::broadcast::channel(1); - let (app_state_sender, _) = tokio::sync::broadcast::channel(1); - #[cfg(any(target_os = "macos", target_os = "linux"))] - let mut offline_app_disk_watch: Option = None; - - #[cfg(any(target_os = "macos", target_os = "linux"))] - { - match watch_offline_app() { - Ok((new_watcher, mut rx)) => { - let sender = app_state_sender.clone(); - tokio::spawn(async move { - while let Some(event) = rx.recv().await { - if let Err(err) = sender.send(event) { - error!("[LLM Resource] Failed to send offline app state: {:?}", err); - } - } - }); - offline_app_disk_watch = Some(new_watcher); - }, - Err(err) => { - error!("[LLM Resource] Failed to watch offline app path: {:?}", err); - }, - } - } - - Self { - user_service, - resource_service: Arc::new(resource_service), - #[cfg(any(target_os = "macos", target_os = "linux"))] - app_disk_watch: offline_app_disk_watch, - app_state_sender, - resource_notify, - } - } - - pub fn subscribe_resource_notify(&self) -> tokio::sync::broadcast::Receiver<()> { - self.resource_notify.subscribe() - } - - pub fn subscribe_app_state(&self) -> tokio::sync::broadcast::Receiver { - self.app_state_sender.subscribe() - } - - /// Returns true when all resources are downloaded and ready to use. - pub async fn is_resource_ready(&self) -> bool { - let sys = get_operating_system(); - if !sys.is_desktop() { - return false; - } - - self - .calculate_pending_resources() - .await - .is_ok_and(|r| r.is_none()) - } - - /// Retrieves model information and updates the current model settings. - pub fn get_llm_setting(&self) -> LocalAISetting { - self.resource_service.retrieve_setting().unwrap_or_default() - } - - #[instrument(level = "info", skip_all, err)] - pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - self.resource_service.store_setting(setting)?; - if let Some(resource) = self.calculate_pending_resources().await? { - let resource = LackOfAIResourcePB::from(resource); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::LocalAIResourceUpdated, - ) - .payload(resource.clone()) - .send(); - return Err(FlowyError::local_ai().with_context(format!("{:?}", resource))); - } - Ok(()) - } - - pub async fn get_lack_of_resource(&self) -> Option { - self - .calculate_pending_resources() - .await - .ok()? - .map(Into::into) - } - - pub async fn calculate_pending_resources(&self) -> FlowyResult> { - let app_path = ollama_plugin_path(); - if !is_plugin_ready() { - trace!("[LLM Resource] offline app not found: {:?}", app_path); - return Ok(Some(PendingResource::PluginExecutableNotReady)); - } - - let setting = self.get_llm_setting(); - let client = Client::builder().timeout(Duration::from_secs(5)).build()?; - - match client.get(&setting.ollama_server_url).send().await { - Ok(resp) if resp.status().is_success() => { - info!( - "[LLM Resource] Ollama server is running at {}", - setting.ollama_server_url - ); - }, - _ => { - info!( - "[LLM Resource] Ollama server is not responding at {}", - setting.ollama_server_url - ); - return Ok(Some(PendingResource::OllamaServerNotReady)); - }, - } - - let required_models = vec![setting.chat_model_name, setting.embedding_model_name]; - - // Query the /api/tags endpoint to get a structured list of locally available models. - let tags_url = format!("{}/api/tags", setting.ollama_server_url); - - match client.get(&tags_url).send().await { - Ok(resp) if resp.status().is_success() => { - let tags: TagsResponse = resp.json().await.inspect_err(|e| { - log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") - })?; - // Check if each of our required models exists in the list of available models - trace!("[LLM Resource] ollama available models: {:?}", tags.models); - for required in &required_models { - if !tags - .models - .iter() - .any(|m| m.name == *required || m.name == format!("{}:latest", required)) - { - log::trace!( - "[LLM Resource] required model '{}' not found in API response", - required - ); - return Ok(Some(PendingResource::MissingModel(required.clone()))); - } - } - }, - _ => { - error!( - "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", - setting.ollama_server_url - ); - return Ok(Some(PendingResource::OllamaServerNotReady)); - }, - } - - Ok(None) - } - - #[instrument(level = "info", skip_all)] - pub async fn get_plugin_config(&self, rag_enabled: bool) -> FlowyResult { - if !self.is_resource_ready().await { - return Err(FlowyError::new( - ErrorCode::AppFlowyLAINotReady, - "AppFlowyLAI not found", - )); - } - - let llm_setting = self.get_llm_setting(); - let bin_path = match get_operating_system() { - OperatingSystem::MacOS | OperatingSystem::Windows | OperatingSystem::Linux => { - ollama_plugin_path() - }, - _ => { - return Err( - FlowyError::local_ai_unavailable() - .with_context("Local AI not available on current platform"), - ); - }, - }; - - let mut config = OllamaPluginConfig::new( - bin_path, - "af_ollama_plugin".to_string(), - llm_setting.chat_model_name.clone(), - llm_setting.embedding_model_name.clone(), - Some(llm_setting.ollama_server_url.clone()), - )?; - - //config = config.with_log_level("debug".to_string()); - - if rag_enabled { - let resource_dir = self.resource_dir()?; - let persist_directory = resource_dir.join("vectorstore"); - if !persist_directory.exists() { - std::fs::create_dir_all(&persist_directory)?; - } - config.set_rag_enabled(&persist_directory)?; - } - - if cfg!(debug_assertions) { - config = config.with_verbose(true); - } - trace!("[AI Chat] config: {:?}", config); - Ok(config) - } - - pub(crate) fn user_model_folder(&self) -> FlowyResult { - self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) - } - - pub(crate) fn resource_dir(&self) -> FlowyResult { - let user_data_dir = self.user_service.application_root_dir()?; - Ok(user_data_dir.join("ai")) - } -} - -#[allow(dead_code)] -fn bytes_to_readable_format(bytes: u64) -> String { - const BYTES_IN_GIGABYTE: u64 = 1024 * 1024 * 1024; - const BYTES_IN_MEGABYTE: u64 = 1024 * 1024; - - if bytes >= BYTES_IN_GIGABYTE { - let gigabytes = (bytes as f64) / (BYTES_IN_GIGABYTE as f64); - format!("{:.1} GB", gigabytes) - } else { - let megabytes = (bytes as f64) / (BYTES_IN_MEGABYTE as f64); - format!("{:.2} MB", megabytes) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs b/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs index fbe4157c8c..76eb01ea6f 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs @@ -1,4 +1,4 @@ -use af_plugin::error::PluginError; +use appflowy_plugin::error::PluginError; use flowy_ai_pub::cloud::QuestionStreamValue; use flowy_error::FlowyError; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 2baed3f0a5..cee8f1d381 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -1,5 +1,4 @@ -use crate::local_ai::resource::WatchDiskEvent; -use af_plugin::core::path::{install_path, ollama_plugin_path}; +use crate::local_ai::local_llm_resource::WatchDiskEvent; use flowy_error::{FlowyError, FlowyResult}; use std::path::PathBuf; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; @@ -21,7 +20,7 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver| match res { Ok(event) => { if event.paths.iter().any(|path| path == &app_path) { @@ -58,3 +57,38 @@ pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver Option { + None +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub(crate) fn install_path() -> Option { + #[cfg(target_os = "windows")] + return None; + + #[cfg(target_os = "macos")] + return Some(PathBuf::from("/usr/local/bin")); + + #[cfg(target_os = "linux")] + return None; +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +pub(crate) fn offline_app_path() -> PathBuf { + PathBuf::new() +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub(crate) fn offline_app_path() -> PathBuf { + let offline_app = "appflowy_ai_plugin"; + #[cfg(target_os = "windows")] + return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + + #[cfg(target_os = "macos")] + return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); + + #[cfg(target_os = "linux")] + return PathBuf::from(format!("/usr/local/bin/{}", offline_app)); +} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs b/frontend/rust-lib/flowy-ai/src/mcp/manager.rs deleted file mode 100644 index 9e40a51f68..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs +++ /dev/null @@ -1,39 +0,0 @@ -use af_mcp::client::{MCPClient, MCPServerConfig}; -use af_mcp::entities::ToolsList; -use dashmap::DashMap; -use flowy_error::FlowyError; -use std::sync::Arc; - -pub struct MCPClientManager { - stdio_clients: Arc>, -} - -impl MCPClientManager { - pub fn new() -> MCPClientManager { - Self { - stdio_clients: Arc::new(DashMap::new()), - } - } - - pub async fn connect_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { - let client = MCPClient::new_stdio(config.clone()).await?; - self.stdio_clients.insert(config.server_cmd, client.clone()); - client.initialize().await?; - Ok(()) - } - - pub async fn remove_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { - let client = self.stdio_clients.remove(&config.server_cmd); - if let Some((_, mut client)) = client { - client.stop().await?; - } - Ok(()) - } - - pub async fn tool_list(&self, server_cmd: &str) -> Option { - let client = self.stdio_clients.get(server_cmd)?; - let tools = client.list_tools().await.ok(); - tracing::trace!("{}: tool list: {:?}", server_cmd, tools); - tools - } -} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs deleted file mode 100644 index 8f73c8326c..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod manager; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index 74f5d5560b..d6dce3696c 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -1,61 +1,93 @@ +use crate::ai_manager::AIUserService; use crate::entities::{ChatStatePB, ModelTypePB}; -use crate::local_ai::controller::LocalAIController; +use crate::local_ai::local_llm_chat::LocalAIController; use crate::notification::{ chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, }; -use af_plugin::error::PluginError; -use flowy_ai_pub::persistence::select_message_content; +use crate::persistence::{select_single_message, ChatMessageTable}; +use appflowy_plugin::error::PluginError; use std::collections::HashMap; use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, - ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, + ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, - UpdateChatParams, + SubscriptionPlan, UpdateChatParams, }; use flowy_error::{FlowyError, FlowyResult}; -use futures::{stream, StreamExt, TryStreamExt}; +use futures::{stream, Sink, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use crate::local_ai::stream_util::QuestionStream; -use flowy_ai_pub::user_service::AIUserService; +use crate::stream_message::StreamMessage; use flowy_storage_pub::storage::StorageService; +use futures_util::SinkExt; use serde_json::{json, Value}; use std::path::Path; use std::sync::{Arc, Weak}; -use tracing::{info, trace}; -use uuid::Uuid; +use tracing::trace; -pub struct ChatServiceMiddleware { +pub struct AICloudServiceMiddleware { cloud_service: Arc, user_service: Arc, - local_ai: Arc, - #[allow(dead_code)] + local_llm_controller: Arc, storage_service: Weak, } -impl ChatServiceMiddleware { +impl AICloudServiceMiddleware { pub fn new( user_service: Arc, cloud_service: Arc, - local_ai: Arc, + local_llm_controller: Arc, storage_service: Weak, ) -> Self { Self { user_service, cloud_service, - local_ai, + local_llm_controller, storage_service, } } - fn get_message_content(&self, message_id: i64) -> FlowyResult { + pub fn is_local_ai_enabled(&self) -> bool { + self.local_llm_controller.is_enabled() + } + + pub async fn index_message_metadata( + &self, + chat_id: &str, + metadata_list: &[ChatMessageMetadata], + index_process_sink: &mut (impl Sink + Unpin), + ) -> Result<(), FlowyError> { + if metadata_list.is_empty() { + return Ok(()); + } + if self.is_local_ai_enabled() { + let _ = index_process_sink + .send(StreamMessage::IndexStart.to_string()) + .await; + + self + .local_llm_controller + .index_message_metadata(chat_id, metadata_list, index_process_sink) + .await?; + let _ = index_process_sink + .send(StreamMessage::IndexEnd.to_string()) + .await; + } else if let Some(_storage_service) = self.storage_service.upgrade() { + // + } + Ok(()) + } + + fn get_message_record(&self, message_id: i64) -> FlowyResult { let uid = self.user_service.user_id()?; let conn = self.user_service.sqlite_connection(uid)?; - let content = select_message_content(conn, message_id)?.ok_or_else(|| { + let row = select_single_message(conn, message_id)?.ok_or_else(|| { FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) })?; - Ok(content) + + Ok(row) } fn handle_plugin_error(&self, err: PluginError) { @@ -65,7 +97,7 @@ impl ChatServiceMiddleware { ) { chat_notification_builder( APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, + ChatNotification::UpdateChatPluginState, ) .payload(ChatStatePB { model_type: ModelTypePB::LocalAI, @@ -77,39 +109,38 @@ impl ChatServiceMiddleware { } #[async_trait] -impl ChatCloudService for ChatServiceMiddleware { +impl ChatCloudService for AICloudServiceMiddleware { async fn create_chat( &self, uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - name: &str, - metadata: serde_json::Value, + workspace_id: &str, + chat_id: &str, + rag_ids: Vec, ) -> Result<(), FlowyError> { self .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) + .create_chat(uid, workspace_id, chat_id, rag_ids) .await } async fn create_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, message_type: ChatMessageType, + metadata: &[ChatMessageMetadata], ) -> Result { self .cloud_service - .create_question(workspace_id, chat_id, message, message_type) + .create_question(workspace_id, chat_id, message, message_type, metadata) .await } async fn create_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, question_id: i64, metadata: Option, @@ -122,67 +153,50 @@ impl ChatCloudService for ChatServiceMiddleware { async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, question_id: i64, format: ResponseFormat, - ai_model: Option, ) -> Result { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; - - info!("stream_answer use model: {:?}", ai_model); - if use_local_ai { - if self.local_ai.is_running() { - let content = self.get_message_content(question_id)?; - match self - .local_ai - .stream_question( - &chat_id.to_string(), - &content, - Some(json!(format)), - json!({}), - ) - .await - { - Ok(stream) => Ok(QuestionStream::new(stream).boxed()), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) + if self.local_llm_controller.is_running() { + let row = self.get_message_record(question_id)?; + match self + .local_llm_controller + .stream_question(chat_id, &row.content, json!([])) + .await + { + Ok(stream) => Ok(QuestionStream::new(stream).boxed()), + Err(err) => { + self.handle_plugin_error(err); + Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) + }, } } else { self .cloud_service - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) + .stream_answer(workspace_id, chat_id, question_id, format) .await } } async fn get_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, ) -> Result { - if self.local_ai.is_running() { - let content = self.get_message_content(question_id)?; + if self.local_llm_controller.is_running() { + let content = self.get_message_record(question_message_id)?.content; match self - .local_ai - .ask_question(&chat_id.to_string(), &content) + .local_llm_controller + .ask_question(chat_id, &content) .await { Ok(answer) => { + // TODO(nathan): metadata let message = self .cloud_service - .create_answer(workspace_id, chat_id, &answer, question_id, None) + .create_answer(workspace_id, chat_id, &answer, question_message_id, None) .await?; Ok(message) }, @@ -194,15 +208,15 @@ impl ChatCloudService for ChatServiceMiddleware { } else { self .cloud_service - .get_answer(workspace_id, chat_id, question_id) + .get_answer(workspace_id, chat_id, question_message_id) .await } } async fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, offset: MessageCursor, limit: u64, ) -> Result { @@ -214,135 +228,113 @@ impl ChatCloudService for ChatServiceMiddleware { async fn get_question_from_answer_id( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, + workspace_id: &str, + chat_id: &str, + answer_id: i64, ) -> Result { self .cloud_service - .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) + .get_question_from_answer_id(workspace_id, chat_id, answer_id) .await } async fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - ai_model: Option, ) -> Result { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; + if self.local_llm_controller.is_running() { + let questions = self + .local_llm_controller + .get_related_question(chat_id) + .await + .map_err(|err| FlowyError::local_ai().with_context(err))?; + trace!("LocalAI related questions: {:?}", questions); - if use_local_ai { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); - - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, - }) - .collect::>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) - } else { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], + let items = questions + .into_iter() + .map(|content| RelatedQuestion { + content, + metadata: None, }) - } + .collect::>(); + + Ok(RepeatedRelatedQuestion { message_id, items }) } else { self .cloud_service - .get_related_message(workspace_id, chat_id, message_id, ai_model) + .get_related_message(workspace_id, chat_id, message_id) .await } } async fn stream_complete( &self, - workspace_id: &Uuid, + workspace_id: &str, params: CompleteTextParams, - ai_model: Option, ) -> Result { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; - - info!("stream_complete use custom model: {:?}", ai_model); - if use_local_ai { - if self.local_ai.is_running() { - match self - .local_ai - .complete_text_v2( - ¶ms.text, - params.completion_type.unwrap() as u8, - Some(json!(params.format)), - Some(json!(params.metadata)), - ) - .await - { - Ok(stream) => Ok( - CompletionStream::new( - stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), - ) - .map_err(FlowyError::from) + if self.local_llm_controller.is_running() { + match self + .local_llm_controller + .complete_text(¶ms.text, params.completion_type.unwrap() as u8) + .await + { + Ok(stream) => Ok( + stream + .map_err(|err| FlowyError::local_ai().with_context(err)) .boxed(), - ), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) + ), + Err(err) => { + self.handle_plugin_error(err); + Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) + }, } } else { self .cloud_service - .stream_complete(workspace_id, params, ai_model) + .stream_complete(workspace_id, params) .await } } - async fn embed_file( + async fn index_file( &self, - workspace_id: &Uuid, + workspace_id: &str, file_path: &Path, - chat_id: &Uuid, + chat_id: &str, metadata: Option>, ) -> Result<(), FlowyError> { - if self.local_ai.is_running() { + if self.local_llm_controller.is_running() { self - .local_ai - .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) + .local_llm_controller + .index_file(chat_id, Some(file_path.to_path_buf()), None, metadata) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; Ok(()) } else { self .cloud_service - .embed_file(workspace_id, file_path, chat_id, metadata) + .index_file(workspace_id, file_path, chat_id, metadata) .await } } + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + self.cloud_service.get_local_ai_config(workspace_id).await + } + + async fn get_workspace_plan( + &self, + workspace_id: &str, + ) -> Result, FlowyError> { + self.cloud_service.get_workspace_plan(workspace_id).await + } + async fn get_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, ) -> Result { self .cloud_service @@ -352,8 +344,8 @@ impl ChatCloudService for ChatServiceMiddleware { async fn update_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, params: UpdateChatParams, ) -> Result<(), FlowyError> { self @@ -362,14 +354,7 @@ impl ChatCloudService for ChatServiceMiddleware { .await } - async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + async fn get_available_models(&self, workspace_id: &str) -> Result { self.cloud_service.get_available_models(workspace_id).await } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { - self - .cloud_service - .get_workspace_default_model(workspace_id) - .await - } } diff --git a/frontend/rust-lib/flowy-ai/src/notification.rs b/frontend/rust-lib/flowy-ai/src/notification.rs index 6fbf3a8e7a..c7857dbc8a 100644 --- a/frontend/rust-lib/flowy-ai/src/notification.rs +++ b/frontend/rust-lib/flowy-ai/src/notification.rs @@ -1,6 +1,5 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; -use tracing::trace; const CHAT_OBSERVABLE_SOURCE: &str = "Chat"; pub const APPFLOWY_AI_NOTIFICATION_KEY: &str = "appflowy_ai_plugin"; @@ -13,10 +12,9 @@ pub enum ChatNotification { DidReceiveChatMessage = 3, StreamChatMessageError = 4, FinishStreaming = 5, - UpdateLocalAIState = 6, - DidUpdateChatSettings = 7, - LocalAIResourceUpdated = 8, - DidUpdateSelectedModel = 9, + UpdateChatPluginState = 6, + UpdateLocalChatAI = 7, + DidUpdateChatSettings = 8, } impl std::convert::From for i32 { @@ -32,20 +30,14 @@ impl std::convert::From for ChatNotification { 3 => ChatNotification::DidReceiveChatMessage, 4 => ChatNotification::StreamChatMessageError, 5 => ChatNotification::FinishStreaming, - 6 => ChatNotification::UpdateLocalAIState, - 7 => ChatNotification::DidUpdateChatSettings, - 8 => ChatNotification::LocalAIResourceUpdated, + 6 => ChatNotification::UpdateChatPluginState, + 7 => ChatNotification::UpdateLocalChatAI, _ => ChatNotification::Unknown, } } } -#[tracing::instrument(level = "trace", skip_all)] -pub(crate) fn chat_notification_builder( - id: T, - ty: ChatNotification, -) -> NotificationBuilder { - let id = id.to_string(); - trace!("chat_notification_builder: id = {id}, ty = {ty:?}"); - NotificationBuilder::new(&id, ty, CHAT_OBSERVABLE_SOURCE) +#[tracing::instrument(level = "trace")] +pub(crate) fn chat_notification_builder(id: &str, ty: ChatNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, CHAT_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-ai/src/offline/mod.rs b/frontend/rust-lib/flowy-ai/src/offline/mod.rs deleted file mode 100644 index e55b43fdb2..0000000000 --- a/frontend/rust-lib/flowy-ai/src/offline/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod offline_message_sync; diff --git a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs deleted file mode 100644 index 55daf6b77f..0000000000 --- a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs +++ /dev/null @@ -1,258 +0,0 @@ -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, - MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, - StreamAnswer, StreamComplete, UpdateChatParams, -}; -use flowy_ai_pub::persistence::{ - update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages, - ChatMessageTable, ChatTable, -}; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::sync::Arc; -use uuid::Uuid; - -pub struct AutoSyncChatService { - cloud_service: Arc, - user_service: Arc, -} - -impl AutoSyncChatService { - pub fn new( - cloud_service: Arc, - user_service: Arc, - ) -> Self { - Self { - cloud_service, - user_service, - } - } - - async fn upsert_message( - &self, - chat_id: &Uuid, - message: ChatMessage, - is_sync: bool, - ) -> Result<(), FlowyError> { - let uid = self.user_service.user_id()?; - let conn = self.user_service.sqlite_connection(uid)?; - let row = ChatMessageTable::from_message(chat_id.to_string(), message, is_sync); - upsert_chat_messages(conn, &[row])?; - Ok(()) - } - - #[allow(dead_code)] - async fn update_message_is_sync( - &self, - chat_id: &Uuid, - message_id: i64, - ) -> Result<(), FlowyError> { - let uid = self.user_service.user_id()?; - let conn = self.user_service.sqlite_connection(uid)?; - update_chat_message_is_sync(conn, &chat_id.to_string(), message_id, true)?; - Ok(()) - } -} - -#[async_trait] -impl ChatCloudService for AutoSyncChatService { - async fn create_chat( - &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - name: &str, - metadata: Value, - ) -> Result<(), FlowyError> { - let conn = self.user_service.sqlite_connection(*uid)?; - let chat = ChatTable::new( - chat_id.to_string(), - metadata.clone(), - rag_ids.clone(), - false, - ); - upsert_chat(conn, &chat)?; - - if self - .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) - .await - .is_ok() - { - let conn = self.user_service.sqlite_connection(*uid)?; - update_chat_is_sync(conn, &chat_id.to_string(), true)?; - } - Ok(()) - } - - async fn create_question( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result { - let message = self - .cloud_service - .create_question(workspace_id, chat_id, message, message_type) - .await?; - self.upsert_message(chat_id, message.clone(), true).await?; - // TODO: implement background sync - // self - // .update_message_is_sync(chat_id, message.message_id) - // .await?; - Ok(message) - } - - async fn create_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option, - ) -> Result { - let message = self - .cloud_service - .create_answer(workspace_id, chat_id, message, question_id, metadata) - .await?; - - // TODO: implement background sync - self.upsert_message(chat_id, message.clone(), true).await?; - Ok(message) - } - - async fn stream_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option, - ) -> Result { - self - .cloud_service - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) - .await - } - - async fn get_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result { - let message = self - .cloud_service - .get_answer(workspace_id, chat_id, question_id) - .await?; - - // TODO: implement background sync - self.upsert_message(chat_id, message.clone(), true).await?; - Ok(message) - } - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result { - self - .cloud_service - .get_chat_messages(workspace_id, chat_id, offset, limit) - .await - } - - async fn get_question_from_answer_id( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result { - self - .cloud_service - .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) - .await - } - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - ai_model: Option, - ) -> Result { - self - .cloud_service - .get_related_message(workspace_id, chat_id, message_id, ai_model) - .await - } - - async fn stream_complete( - &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option, - ) -> Result { - self - .cloud_service - .stream_complete(workspace_id, params, ai_model) - .await - } - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option>, - ) -> Result<(), FlowyError> { - self - .cloud_service - .embed_file(workspace_id, file_path, chat_id, metadata) - .await - } - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result { - // TODO: implement background sync - self - .cloud_service - .get_chat_settings(workspace_id, chat_id) - .await - } - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError> { - // TODO: implement background sync - self - .cloud_service - .update_chat_settings(workspace_id, chat_id, params) - .await - } - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result { - self.cloud_service.get_available_models(workspace_id).await - } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { - self - .cloud_service - .get_workspace_default_model(workspace_id) - .await - } -} diff --git a/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs new file mode 100644 index 0000000000..aa4dd8215d --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_message_sql.rs @@ -0,0 +1,94 @@ +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::upsert::excluded; +use flowy_sqlite::{ + diesel, insert_into, + query_dsl::*, + schema::{chat_message_table, chat_message_table::dsl}, + DBConnection, ExpressionMethods, Identifiable, Insertable, OptionalExtension, QueryResult, + Queryable, +}; + +#[derive(Queryable, Insertable, Identifiable)] +#[diesel(table_name = chat_message_table)] +#[diesel(primary_key(message_id))] +pub struct ChatMessageTable { + pub message_id: i64, + pub chat_id: String, + pub content: String, + pub created_at: i64, + pub author_type: i64, + pub author_id: String, + pub reply_message_id: Option, + pub metadata: Option, +} + +pub fn insert_chat_messages( + mut conn: DBConnection, + new_messages: &[ChatMessageTable], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + for message in new_messages { + let _ = insert_into(chat_message_table::table) + .values(message) + .on_conflict(chat_message_table::message_id) + .do_update() + .set(( + chat_message_table::content.eq(excluded(chat_message_table::content)), + chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), + chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), + chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), + chat_message_table::reply_message_id.eq(excluded(chat_message_table::reply_message_id)), + )) + .execute(conn)?; + } + Ok::<(), FlowyError>(()) + })?; + + Ok(()) +} + +pub fn select_chat_messages( + mut conn: DBConnection, + chat_id_val: &str, + limit_val: i64, + after_message_id: Option, + before_message_id: Option, +) -> QueryResult> { + let mut query = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .into_boxed(); + if let Some(after_message_id) = after_message_id { + query = query.filter(chat_message_table::message_id.gt(after_message_id)); + } + + if let Some(before_message_id) = before_message_id { + query = query.filter(chat_message_table::message_id.lt(before_message_id)); + } + query = query + .order((chat_message_table::message_id.desc(),)) + .limit(limit_val); + + let messages: Vec = query.load::(&mut *conn)?; + Ok(messages) +} + +pub fn select_single_message( + mut conn: DBConnection, + message_id_val: i64, +) -> QueryResult> { + let message = dsl::chat_message_table + .filter(chat_message_table::message_id.eq(message_id_val)) + .first::(&mut *conn) + .optional()?; + Ok(message) +} + +pub fn select_message_where_match_reply_message_id( + mut conn: DBConnection, + answer_message_id_val: i64, +) -> QueryResult> { + dsl::chat_message_table + .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) + .first::(&mut *conn) + .optional() +} diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs similarity index 58% rename from frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs rename to frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs index f5398c48c0..e962f2c880 100644 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs +++ b/frontend/rust-lib/flowy-ai/src/persistence/chat_sql.rs @@ -7,10 +7,7 @@ use flowy_sqlite::{ schema::{chat_table, chat_table::dsl}, AsChangeset, DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, }; -use lib_infra::util::timestamp; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use uuid::Uuid; #[derive(Clone, Default, Queryable, Insertable, Identifiable)] #[diesel(table_name = chat_table)] @@ -19,25 +16,10 @@ pub struct ChatTable { pub chat_id: String, pub created_at: i64, pub name: String, + pub local_files: String, pub metadata: String, - pub rag_ids: Option, - pub is_sync: bool, -} - -impl ChatTable { - pub fn new(chat_id: String, metadata: Value, rag_ids: Vec, is_sync: bool) -> Self { - let rag_ids = rag_ids.iter().map(|v| v.to_string()).collect::>(); - let metadata = serialize_chat_metadata(&metadata); - let rag_ids = Some(serialize_rag_ids(&rag_ids)); - Self { - chat_id, - created_at: timestamp(), - name: "".to_string(), - metadata, - rag_ids, - is_sync, - } - } + pub local_enabled: bool, + pub sync_to_cloud: bool, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -67,37 +49,22 @@ pub struct ChatTableFile { pub struct ChatTableChangeset { pub chat_id: String, pub name: Option, + pub local_files: Option, pub metadata: Option, - pub rag_ids: Option, - pub is_sync: Option, + pub local_enabled: Option, + pub sync_to_cloud: Option, } -pub fn serialize_rag_ids(rag_ids: &[String]) -> String { - serde_json::to_string(rag_ids).unwrap_or_default() -} - -pub fn deserialize_rag_ids(rag_ids_str: &Option) -> Vec { - match rag_ids_str { - Some(str) => serde_json::from_str(str).unwrap_or_default(), - None => Vec::new(), +impl ChatTableChangeset { + pub fn from_metadata(metadata: ChatTableMetadata) -> Self { + ChatTableChangeset { + metadata: serde_json::to_string(&metadata).ok(), + ..Default::default() + } } } -pub fn deserialize_chat_metadata(metadata: &str) -> T -where - T: serde::de::DeserializeOwned + Default, -{ - serde_json::from_str(metadata).unwrap_or_default() -} - -pub fn serialize_chat_metadata(metadata: &T) -> String -where - T: Serialize, -{ - serde_json::to_string(metadata).unwrap_or_default() -} - -pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { +pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult { diesel::insert_into(chat_table::table) .values(new_chat) .on_conflict(chat_table::chat_id) @@ -105,13 +72,11 @@ pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult< .set(( chat_table::created_at.eq(excluded(chat_table::created_at)), chat_table::name.eq(excluded(chat_table::name)), - chat_table::metadata.eq(excluded(chat_table::metadata)), - chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), - chat_table::is_sync.eq(excluded(chat_table::is_sync)), )) .execute(&mut *conn) } +#[allow(dead_code)] pub fn update_chat( conn: &mut SqliteConnection, changeset: ChatTableChangeset, @@ -121,16 +86,7 @@ pub fn update_chat( Ok(affected_row) } -pub fn update_chat_is_sync( - mut conn: DBConnection, - chat_id_val: &str, - is_sync_val: bool, -) -> QueryResult { - diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) - .set(chat_table::is_sync.eq(is_sync_val)) - .execute(&mut *conn) -} - +#[allow(dead_code)] pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult { let row = dsl::chat_table .filter(chat_table::chat_id.eq(chat_id_val)) @@ -138,17 +94,7 @@ pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult FlowyResult> { - let chat = dsl::chat_table - .filter(chat_table::chat_id.eq(chat_id_val)) - .first::(conn)?; - - Ok(deserialize_rag_ids(&chat.rag_ids)) -} - +#[allow(dead_code)] pub fn read_chat_metadata( conn: &mut SqliteConnection, chat_id_val: &str, @@ -157,7 +103,8 @@ pub fn read_chat_metadata( .select(chat_table::metadata) .filter(chat_table::chat_id.eq(chat_id_val)) .first::(&mut *conn)?; - Ok(deserialize_chat_metadata(&metadata_str)) + let value = serde_json::from_str(&metadata_str).unwrap_or_default(); + Ok(value) } #[allow(dead_code)] diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs b/frontend/rust-lib/flowy-ai/src/persistence/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs rename to frontend/rust-lib/flowy-ai/src/persistence/mod.rs diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs index 3f7b37bd34..c507262b85 100644 --- a/frontend/rust-lib/flowy-ai/src/stream_message.rs +++ b/frontend/rust-lib/flowy-ai/src/stream_message.rs @@ -1,14 +1,10 @@ use std::fmt::Display; -#[allow(dead_code)] pub enum StreamMessage { MessageId(i64), IndexStart, IndexEnd, Text(String), - OnData(String), - OnError(String), - Metadata(String), Done, StartIndexFile { file_name: String }, EndIndexFile { file_name: String }, @@ -24,10 +20,7 @@ impl Display for StreamMessage { StreamMessage::Text(text) => { write!(f, "data:{}", text) }, - StreamMessage::OnData(message) => write!(f, "data:{message}"), - StreamMessage::OnError(message) => write!(f, "error:{message}"), StreamMessage::Done => write!(f, "done:"), - StreamMessage::Metadata(s) => write!(f, "metadata:{s}"), StreamMessage::StartIndexFile { file_name } => { write!(f, "start_index_file:{}", file_name) }, diff --git a/frontend/rust-lib/flowy-ai/src/util.rs b/frontend/rust-lib/flowy-ai/src/util.rs deleted file mode 100644 index a181d1b1d3..0000000000 --- a/frontend/rust-lib/flowy-ai/src/util.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn ai_available_models_key(object_id: &str) -> String { - format!("ai_models_{}", object_id) -} diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index b4e7bd5fec..d1668524cb 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -32,13 +32,13 @@ collab = { workspace = true } #collab = { workspace = true, features = ["verbose_log"] } diesel.workspace = true +uuid.workspace = true flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } client-api.workspace = true flowy-ai = { workspace = true } flowy-ai-pub = { workspace = true } -af-local-ai = { workspace = true } -af-plugin = { workspace = true } +appflowy-local-ai = { version = "0.1.0" } tracing.workspace = true @@ -56,10 +56,10 @@ lib-infra = { workspace = true } serde.workspace = true serde_json.workspace = true serde_repr.workspace = true -uuid.workspace = true +futures.workspace = true +walkdir = "2.4.0" sysinfo = "0.30.5" semver = { version = "1.0.22", features = ["serde"] } -url = "2.5.0" [features] profiling = ["console-subscriber", "tokio/tracing"] @@ -74,6 +74,14 @@ dart = [ "flowy-ai/dart", "flowy-storage/dart", ] +ts = [ + "flowy-user/tauri_ts", + "flowy-folder/tauri_ts", + "flowy-search/tauri_ts", + "flowy-database2/ts", + "flowy-ai/tauri_ts", + "flowy-storage/tauri_ts", +] openssl_vendored = ["flowy-sqlite/openssl_vendored"] # Enable/Disable AppFlowy Verbose Log Configuration diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 2bad578627..2b379ab63a 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -1,10 +1,9 @@ use std::fmt; -use std::path::{Path, PathBuf}; +use std::path::Path; use base64::Engine; use semver::Version; use tracing::{error, info}; -use url::Url; use crate::log_filter::create_log_filter; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; @@ -29,25 +28,7 @@ pub struct AppFlowyCoreConfig { pub(crate) log_filter: String, pub cloud_config: Option, } -impl AppFlowyCoreConfig { - pub fn ensure_path(&self) { - let create_if_needed = |path_str: &str, label: &str| { - let dir = std::path::Path::new(path_str); - if !dir.exists() { - match std::fs::create_dir_all(dir) { - Ok(_) => info!("Created {} path: {}", label, path_str), - Err(err) => error!( - "Failed to create {} path: {}. Error: {}", - label, path_str, err - ), - } - } - }; - create_if_needed(&self.storage_path, "storage"); - create_if_needed(&self.application_path, "application"); - } -} impl fmt::Debug for AppFlowyCoreConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("AppFlowy Configuration"); @@ -65,60 +46,30 @@ impl fmt::Debug for AppFlowyCoreConfig { } fn make_user_data_folder(root: &str, url: &str) -> String { - // If a URL is provided, try to parse it and extract the domain name. - // This isolates the user data folder by the domain, which prevents data sharing - // between different AppFlowy cloud instances. - print!("Creating user data folder for URL: {}, root:{}", url, root); - let mut storage_path = if url.is_empty() { - PathBuf::from(root) - } else { + // Isolate the user data folder by using the base url of AppFlowy cloud. This is to avoid + // the user data folder being shared by different AppFlowy cloud. + let storage_path = if !url.is_empty() { let server_base64 = URL_SAFE_ENGINE.encode(url); - PathBuf::from(format!("{}_{}", root, server_base64)) + format!("{}_{}", root, server_base64) + } else { + root.to_string() }; - // Only use new storage path if the old one doesn't exist - if !storage_path.exists() { - let anon_path = format!("{}_anonymous", root); - // We use domain name as suffix to isolate the user data folder since version 0.8.9 - let new_storage_path = if url.is_empty() { - // if the url is empty, then it's anonymous mode - anon_path - } else { - match Url::parse(url) { - Ok(parsed_url) => { - if let Some(domain) = parsed_url.host_str() { - format!("{}_{}", root, domain) - } else { - anon_path - } - }, - Err(_) => anon_path, - } - }; - - storage_path = PathBuf::from(new_storage_path); - } - // Copy the user data folder from the root path to the isolated path // The root path without any suffix is the created by the local version AppFlowy - if !storage_path.exists() && Path::new(root).exists() { - info!("Copy dir from {} to {:?}", root, storage_path); + if !Path::new(&storage_path).exists() && Path::new(root).exists() { + info!("Copy dir from {} to {}", root, storage_path); let src = Path::new(root); - match copy_dir_recursive(src, &storage_path) { - Ok(_) => storage_path - .into_os_string() - .into_string() - .unwrap_or_else(|_| root.to_string()), + match copy_dir_recursive(src, Path::new(&storage_path)) { + Ok(_) => storage_path, Err(err) => { + // when the copy dir failed, use the root path as the storage path error!("Copy dir failed: {}", err); root.to_string() }, } } else { storage_path - .into_os_string() - .into_string() - .unwrap_or_else(|_| root.to_string()) } } @@ -132,11 +83,15 @@ impl AppFlowyCoreConfig { name: String, ) -> Self { let cloud_config = AFCloudConfiguration::from_env().ok(); - // By default enable sync trace log - let log_crates = vec!["sync_trace_log".to_string()]; + let mut log_crates = vec![]; let storage_path = match &cloud_config { None => custom_application_path, - Some(config) => make_user_data_folder(&custom_application_path, &config.base_url), + Some(config) => { + if config.enable_sync_trace { + log_crates.push("sync_trace_log".to_string()); + } + make_user_data_folder(&custom_application_path, &config.base_url) + }, }; let log_filter = create_log_filter( diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index a7d2bc15c1..f9e4befeb0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -5,11 +5,9 @@ use collab::preclude::{Collab, StateVector}; use collab::util::is_change_since_sv; use collab_entity::CollabType; use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; -use flowy_ai::ai_manager::{AIExternalService, AIManager}; -use flowy_ai::local_ai::controller::LocalAIController; +use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService}; use flowy_ai_pub::cloud::ChatCloudService; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyError; use flowy_folder::ViewLayout; use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams}; use flowy_folder_pub::query::FolderService; @@ -23,7 +21,6 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Weak}; use tracing::{error, info}; -use uuid::Uuid; pub struct ChatDepsResolver; @@ -35,7 +32,6 @@ impl ChatDepsResolver { storage_service: Weak, folder_cloud_service: Arc, folder_service: impl FolderService, - local_ai: Arc, ) -> Arc { let user_service = ChatUserServiceImpl(authenticate_user); Arc::new(AIManager::new( @@ -47,7 +43,6 @@ impl ChatDepsResolver { folder_service: Box::new(folder_service), folder_cloud_service, }, - local_ai, )) } } @@ -61,9 +56,9 @@ struct ChatQueryServiceImpl { impl AIExternalService for ChatQueryServiceImpl { async fn query_chat_rag_ids( &self, - parent_view_id: &Uuid, - chat_id: &Uuid, - ) -> Result, FlowyError> { + parent_view_id: &str, + chat_id: &str, + ) -> Result, FlowyError> { let mut ids = self .folder_service .get_surrounding_view_ids_with_view_layout(parent_view_id, ViewLayout::Document) @@ -77,9 +72,9 @@ impl AIExternalService for ChatQueryServiceImpl { } async fn sync_rag_documents( &self, - workspace_id: &Uuid, - rag_ids: Vec, - mut rag_metadata_map: HashMap, + workspace_id: &str, + rag_ids: Vec, + mut rag_metadata_map: HashMap, ) -> Result, FlowyError> { let mut result = Vec::new(); @@ -101,7 +96,7 @@ impl AIExternalService for ChatQueryServiceImpl { if let Ok(prev_sv) = StateVector::decode_v1(&metadata.prev_sync_state_vector) { let collab = Collab::new_with_source( CollabOrigin::Empty, - &rag_id.to_string(), + &rag_id, DataSource::DocStateV1(query_collab.encoded_collab.doc_state.to_vec()), vec![], false, @@ -116,7 +111,7 @@ impl AIExternalService for ChatQueryServiceImpl { // Perform full sync if changes are detected or no state vector is found let params = FullSyncCollabParams { - object_id: rag_id, + object_id: rag_id.clone(), collab_type: CollabType::Document, encoded_collab: query_collab.encoded_collab.clone(), }; @@ -130,7 +125,7 @@ impl AIExternalService for ChatQueryServiceImpl { } else { info!("[Chat] full sync rag document: {}", rag_id); result.push(AFCollabMetadata { - object_id: rag_id.to_string(), + object_id: rag_id, updated_at: timestamp(), prev_sync_state_vector: query_collab.encoded_collab.state_vector.to_vec(), collab_type: CollabType::Document as i32, @@ -141,7 +136,7 @@ impl AIExternalService for ChatQueryServiceImpl { Ok(result) } - async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError> { + async fn notify_did_send_message(&self, chat_id: &str, message: &str) -> Result<(), FlowyError> { info!( "notify_did_send_message: chat_id: {}, message: {}", chat_id, message @@ -154,7 +149,7 @@ impl AIExternalService for ChatQueryServiceImpl { } } -pub struct ChatUserServiceImpl(Weak); +struct ChatUserServiceImpl(Weak); impl ChatUserServiceImpl { fn upgrade_user(&self) -> Result, FlowyError> { let user = self @@ -165,17 +160,16 @@ impl ChatUserServiceImpl { } } -#[async_trait] impl AIUserService for ChatUserServiceImpl { fn user_id(&self) -> Result { self.upgrade_user()?.user_id() } - async fn is_local_model(&self) -> FlowyResult { - self.upgrade_user()?.is_local_mode().await + fn device_id(&self) -> Result { + self.upgrade_user()?.device_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs index c49757f735..22342615e3 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs @@ -1,22 +1,29 @@ -use crate::server_layer::ServerProvider; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::search_dto::SearchDocumentResponseItem; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; use collab::core::origin::{CollabClient, CollabOrigin}; use collab::entity::EncodedCollab; use collab::preclude::CollabPlugin; use collab_entity::CollabType; +use flowy_search_pub::cloud::SearchCloudService; +use serde_json::Value; +use std::collections::HashMap; +use std::path::Path; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; +use tokio_stream::wrappers::WatchStream; +use tracing::{debug, info}; + use collab_integrate::collab_builder::{ CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, }; -use flowy_ai_pub::cloud::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, - MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, - UpdateChatParams, + ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + CompleteTextParams, LocalAIConfig, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, + StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, @@ -26,27 +33,18 @@ use flowy_document::deps::DocumentData; use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, + FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, FullSyncCollabParams, + Workspace, WorkspaceRecord, }; use flowy_folder_pub::entities::PublishPayload; -use flowy_search_pub::cloud::SearchCloudService; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; -use flowy_user_pub::entities::{AuthType, UserTokenState}; +use flowy_user_pub::entities::{Authenticator, UserTokenState}; use lib_infra::async_trait::async_trait; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; -use tokio_stream::wrappers::WatchStream; -use tracing::log::error; -use tracing::{debug, info}; -use uuid::Uuid; + +use crate::server_layer::{Server, ServerProvider}; #[async_trait] impl StorageCloudService for ServerProvider { @@ -84,7 +82,7 @@ impl StorageCloudService for ServerProvider { async fn get_object_url_v1( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, ) -> FlowyResult { @@ -95,7 +93,7 @@ impl StorageCloudService for ServerProvider { .await } - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { + async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)> { self .get_server() .ok()? @@ -106,7 +104,7 @@ impl StorageCloudService for ServerProvider { async fn create_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, content_type: &str, @@ -121,7 +119,7 @@ impl StorageCloudService for ServerProvider { async fn upload_part( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -144,7 +142,7 @@ impl StorageCloudService for ServerProvider { async fn complete_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -161,7 +159,6 @@ impl StorageCloudService for ServerProvider { impl UserCloudServiceProvider for ServerProvider { fn set_token(&self, token: &str) -> Result<(), FlowyError> { let server = self.get_server()?; - info!("Set token"); server.set_token(token)?; Ok(()) } @@ -186,22 +183,18 @@ impl UserCloudServiceProvider for ServerProvider { } } - /// When user login, the provider type is set by the [AuthType] and save to disk for next use. + /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. /// - /// Each [AuthType] has a corresponding [AuthType]. The [AuthType] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, + /// Each [Authenticator] has a corresponding [Server]. The [Server] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [Server] is set, /// it will be used when user open the app again. /// - fn set_server_auth_type(&self, auth_type: &AuthType, token: Option) -> FlowyResult<()> { - self.set_auth_type(*auth_type); - if let Some(token) = token { - self.set_token(&token)?; - } - Ok(()) + fn set_user_authenticator(&self, authenticator: &Authenticator) { + self.set_authenticator(authenticator.clone()); } - fn get_server_auth_type(&self) -> AuthType { - self.get_auth_type() + fn get_user_authenticator(&self) -> Authenticator { + self.get_authenticator() } fn set_network_reachable(&self, reachable: bool) { @@ -215,7 +208,7 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.set_secret(secret); } - /// Returns the [UserCloudService] base on the current [AuthType]. + /// Returns the [UserCloudService] base on the current [Server]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let user_service = self.get_server()?.user_service(); @@ -223,9 +216,9 @@ impl UserCloudServiceProvider for ServerProvider { } fn service_url(&self) -> String { - match self.get_auth_type() { - AuthType::Local => "".to_string(), - AuthType::AppFlowyCloud => AFCloudConfiguration::from_env() + match self.get_server_type() { + Server::Local => "".to_string(), + Server::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), } @@ -234,13 +227,44 @@ impl UserCloudServiceProvider for ServerProvider { #[async_trait] impl FolderCloudService for ServerProvider { + async fn create_workspace(&self, uid: i64, name: &str) -> Result { + let server = self.get_server()?; + let name = name.to_string(); + server.folder_service().create_workspace(uid, &name).await + } + + async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server()?; + server.folder_service().open_workspace(&workspace_id).await + } + + async fn get_all_workspace(&self) -> Result, FlowyError> { + let server = self.get_server()?; + server.folder_service().get_all_workspace().await + } + + async fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> Result, FlowyError> { + let server = self.get_server()?; + + server + .folder_service() + .get_folder_data(workspace_id, uid) + .await + } + async fn get_folder_snapshots( &self, workspace_id: &str, limit: usize, ) -> Result, FlowyError> { - self - .get_server()? + let server = self.get_server()?; + + server .folder_service() .get_folder_snapshots(workspace_id, limit) .await @@ -248,33 +272,22 @@ impl FolderCloudService for ServerProvider { async fn get_folder_doc_state( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, collab_type: CollabType, - object_id: &Uuid, + object_id: &str, ) -> Result, FlowyError> { - self - .get_server()? + let server = self.get_server()?; + + server .folder_service() .get_folder_doc_state(workspace_id, uid, collab_type, object_id) .await } - async fn full_sync_collab_object( - &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .full_sync_collab_object(workspace_id, params) - .await - } - async fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -294,11 +307,12 @@ impl FolderCloudService for ServerProvider { async fn publish_view( &self, - workspace_id: &Uuid, + workspace_id: &str, payload: Vec, ) -> Result<(), FlowyError> { - self - .get_server()? + let server = self.get_server()?; + + server .folder_service() .publish_view(workspace_id, payload) .await @@ -306,29 +320,29 @@ impl FolderCloudService for ServerProvider { async fn unpublish_views( &self, - workspace_id: &Uuid, - view_ids: Vec, + workspace_id: &str, + view_ids: Vec, ) -> Result<(), FlowyError> { - self - .get_server()? + let server = self.get_server()?; + server .folder_service() .unpublish_views(workspace_id, view_ids) .await } - async fn get_publish_info(&self, view_id: &Uuid) -> Result { + async fn get_publish_info(&self, view_id: &str) -> Result { let server = self.get_server()?; server.folder_service().get_publish_info(view_id).await } async fn set_publish_name( &self, - workspace_id: &Uuid, - view_id: Uuid, + workspace_id: &str, + view_id: String, new_name: String, ) -> Result<(), FlowyError> { - self - .get_server()? + let server = self.get_server()?; + server .folder_service() .set_publish_name(workspace_id, view_id, new_name) .await @@ -336,20 +350,28 @@ impl FolderCloudService for ServerProvider { async fn set_publish_namespace( &self, - workspace_id: &Uuid, + workspace_id: &str, new_namespace: String, ) -> Result<(), FlowyError> { - self - .get_server()? + let server = self.get_server()?; + server .folder_service() .set_publish_namespace(workspace_id, new_namespace) .await } + async fn get_publish_namespace(&self, workspace_id: &str) -> Result { + let server = self.get_server()?; + server + .folder_service() + .get_publish_namespace(workspace_id) + .await + } + /// List all published views of the current workspace. async fn list_published_views( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -360,7 +382,7 @@ impl FolderCloudService for ServerProvider { async fn get_default_published_view_info( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result { let server = self.get_server()?; server @@ -371,7 +393,7 @@ impl FolderCloudService for ServerProvider { async fn set_default_published_view( &self, - workspace_id: &Uuid, + workspace_id: &str, view_id: uuid::Uuid, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -381,7 +403,7 @@ impl FolderCloudService for ServerProvider { .await } - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError> { let server = self.get_server()?; server .folder_service() @@ -389,14 +411,6 @@ impl FolderCloudService for ServerProvider { .await } - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { - let server = self.get_server()?; - server - .folder_service() - .get_publish_namespace(workspace_id) - .await - } - async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { self .get_server()? @@ -404,28 +418,42 @@ impl FolderCloudService for ServerProvider { .import_zip(file_path) .await } + + async fn full_sync_collab_object( + &self, + workspace_id: &str, + params: FullSyncCollabParams, + ) -> Result<(), FlowyError> { + self + .get_server()? + .folder_service() + .full_sync_collab_object(workspace_id, params) + .await + } } #[async_trait] impl DatabaseCloudService for ServerProvider { async fn get_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError> { + let workspace_id = workspace_id.to_string(); let server = self.get_server()?; + let database_id = object_id.to_string(); server .database_service() - .get_database_encode_collab(object_id, collab_type, workspace_id) + .get_database_encode_collab(&database_id, collab_type, &workspace_id) .await } async fn create_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -437,28 +465,30 @@ impl DatabaseCloudService for ServerProvider { async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result { + let workspace_id = workspace_id.to_string(); let server = self.get_server()?; server .database_service() - .batch_get_database_encode_collab(object_ids, object_ty, workspace_id) + .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) .await } async fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, + object_id: &str, limit: usize, ) -> Result, FlowyError> { let server = self.get_server()?; + let database_id = object_id.to_string(); server .database_service() - .get_database_collab_object_snapshots(object_id, limit) + .get_database_collab_object_snapshots(&database_id, limit) .await } } @@ -467,29 +497,29 @@ impl DatabaseCloudService for ServerProvider { impl DatabaseAIService for ServerProvider { async fn summary_database_row( &self, - _workspace_id: &Uuid, - _object_id: &Uuid, - _summary_row: SummaryRowContent, + workspace_id: &str, + object_id: &str, + summary_row: SummaryRowContent, ) -> Result { self .get_server()? .database_ai_service() .ok_or_else(FlowyError::not_support)? - .summary_database_row(_workspace_id, _object_id, _summary_row) + .summary_database_row(workspace_id, object_id, summary_row) .await } async fn translate_database_row( &self, - _workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, ) -> Result { self .get_server()? .database_ai_service() .ok_or_else(FlowyError::not_support)? - .translate_database_row(_workspace_id, _translate_row, _language) + .translate_database_row(workspace_id, translate_row, language) .await } } @@ -498,8 +528,8 @@ impl DatabaseAIService for ServerProvider { impl DocumentCloudService for ServerProvider { async fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -510,7 +540,7 @@ impl DocumentCloudService for ServerProvider { async fn get_document_snapshots( &self, - document_id: &Uuid, + document_id: &str, limit: usize, workspace_id: &str, ) -> Result, FlowyError> { @@ -524,8 +554,8 @@ impl DocumentCloudService for ServerProvider { async fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError> { let server = self.get_server()?; server @@ -536,8 +566,8 @@ impl DocumentCloudService for ServerProvider { async fn create_document_collab( &self, - workspace_id: &Uuid, - document_id: &Uuid, + workspace_id: &str, + document_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let server = self.get_server()?; @@ -550,15 +580,12 @@ impl DocumentCloudService for ServerProvider { impl CollabCloudPluginProvider for ServerProvider { fn provider_type(&self) -> CollabPluginProviderType { - match self.get_auth_type() { - AuthType::Local => CollabPluginProviderType::Local, - AuthType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, - } + self.get_server_type().into() } fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { // If the user is local, we don't need to create a sync plugin. - if self.get_auth_type().is_local() { + if self.get_server_type().is_local() { debug!( "User authenticator is local, skip create sync plugin for: {}", context @@ -584,37 +611,26 @@ impl CollabCloudPluginProvider for ServerProvider { collab_object.uid, collab_object.device_id.clone(), )); - - if let (Ok(object_id), Ok(workspace_id)) = ( - Uuid::from_str(&collab_object.object_id), - Uuid::from_str(&collab_object.workspace_id), - ) { - let sync_object = SyncObject::new( - object_id, - workspace_id, - collab_object.collab_type, - &collab_object.device_id, - ); - let (sink, stream) = (channel.sink(), channel.stream()); - let sink_config = SinkConfig::new().send_timeout(8); - let sync_plugin = SyncPlugin::new( - origin, - sync_object, - local_collab, - sink, - sink_config, - stream, - Some(channel), - ws_connect_state, - Some(Duration::from_secs(60)), - ); - plugins.push(Box::new(sync_plugin)); - } else { - error!( - "Failed to parse collab object id: {}", - collab_object.object_id - ); - } + let sync_object = SyncObject::new( + &collab_object.object_id, + &collab_object.workspace_id, + collab_object.collab_type, + &collab_object.device_id, + ); + let (sink, stream) = (channel.sink(), channel.stream()); + let sink_config = SinkConfig::new().send_timeout(8); + let sync_plugin = SyncPlugin::new( + origin, + sync_object, + local_collab, + sink, + sink_config, + stream, + Some(channel), + ws_connect_state, + Some(Duration::from_secs(60)), + ); + plugins.push(Box::new(sync_plugin)); }, Ok(None) => { tracing::error!("🔴Failed to get collab ws channel: channel is none"); @@ -639,38 +655,39 @@ impl ChatCloudService for ServerProvider { async fn create_chat( &self, uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - name: &str, - metadata: serde_json::Value, + workspace_id: &str, + chat_id: &str, + rag_ids: Vec, ) -> Result<(), FlowyError> { let server = self.get_server(); server? .chat_service() - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) + .create_chat(uid, workspace_id, chat_id, rag_ids) .await } async fn create_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, message_type: ChatMessageType, + metadata: &[ChatMessageMetadata], ) -> Result { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); let message = message.to_string(); self .get_server()? .chat_service() - .create_question(workspace_id, chat_id, &message, message_type) + .create_question(&workspace_id, &chat_id, &message, message_type, metadata) .await } async fn create_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, question_id: i64, metadata: Option, @@ -684,23 +701,24 @@ impl ChatCloudService for ServerProvider { async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + message_id: i64, format: ResponseFormat, - ai_model: Option, ) -> Result { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) + .stream_answer(&workspace_id, &chat_id, message_id, format) .await } async fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, offset: MessageCursor, limit: u64, ) -> Result { @@ -713,8 +731,8 @@ impl ChatCloudService for ServerProvider { async fn get_question_from_answer_id( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, answer_message_id: i64, ) -> Result { self @@ -726,62 +744,80 @@ impl ChatCloudService for ServerProvider { async fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - ai_model: Option, ) -> Result { self .get_server()? .chat_service() - .get_related_message(workspace_id, chat_id, message_id, ai_model) + .get_related_message(workspace_id, chat_id, message_id) .await } async fn get_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, ) -> Result { let server = self.get_server(); server? .chat_service() - .get_answer(workspace_id, chat_id, question_id) + .get_answer(workspace_id, chat_id, question_message_id) .await } async fn stream_complete( &self, - workspace_id: &Uuid, + workspace_id: &str, params: CompleteTextParams, - ai_model: Option, ) -> Result { + let workspace_id = workspace_id.to_string(); let server = self.get_server()?; server .chat_service() - .stream_complete(workspace_id, params, ai_model) + .stream_complete(&workspace_id, params) .await } - async fn embed_file( + async fn index_file( &self, - workspace_id: &Uuid, + workspace_id: &str, file_path: &Path, - chat_id: &Uuid, + chat_id: &str, metadata: Option>, ) -> Result<(), FlowyError> { self .get_server()? .chat_service() - .embed_file(workspace_id, file_path, chat_id, metadata) + .index_file(workspace_id, file_path, chat_id, metadata) + .await + } + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + self + .get_server()? + .chat_service() + .get_local_ai_config(workspace_id) + .await + } + + async fn get_workspace_plan( + &self, + workspace_id: &str, + ) -> Result, FlowyError> { + self + .get_server()? + .chat_service() + .get_workspace_plan(workspace_id) .await } async fn get_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, ) -> Result { self .get_server()? @@ -792,8 +828,8 @@ impl ChatCloudService for ServerProvider { async fn update_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, params: UpdateChatParams, ) -> Result<(), FlowyError> { self @@ -803,28 +839,20 @@ impl ChatCloudService for ServerProvider { .await } - async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + async fn get_available_models(&self, workspace_id: &str) -> Result { self .get_server()? .chat_service() .get_available_models(workspace_id) .await } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { - self - .get_server()? - .chat_service() - .get_workspace_default_model(workspace_id) - .await - } } #[async_trait] impl SearchCloudService for ServerProvider { async fn document_search( &self, - workspace_id: &Uuid, + workspace_id: &str, query: String, ) -> Result, FlowyError> { let server = self.get_server()?; @@ -833,21 +861,4 @@ impl SearchCloudService for ServerProvider { None => Err(FlowyError::internal().with_context("SearchCloudService not found")), } } - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec, - ) -> Result { - let server = self.get_server()?; - match server.search_service() { - Some(search_service) => { - search_service - .generate_search_summary(workspace_id, query, search_results) - .await - }, - None => Err(FlowyError::internal().with_context("SearchCloudService not found")), - } - } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index 078ee7359b..a8827e06b0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -13,7 +13,6 @@ use collab_integrate::collab_builder::WorkspaceCollabIntegrate; use lib_infra::util::timestamp; use std::sync::{Arc, Weak}; use tracing::debug; -use uuid::Uuid; pub struct SnapshotDBImpl(pub Weak); @@ -25,7 +24,7 @@ impl SnapshotPersistence for SnapshotDBImpl { collab_type: &CollabType, encoded_v1: Vec, ) -> Result<(), PersistenceError> { - let collab_type = *collab_type; + let collab_type = collab_type.clone(); let object_id = object_id.to_string(); let weak_user = self.0.clone(); tokio::task::spawn_blocking(move || { @@ -223,12 +222,12 @@ impl WorkspaceCollabIntegrateImpl { } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { let workspace_id = self.upgrade_user()?.workspace_id()?; Ok(workspace_id) } - fn device_id(&self) -> Result { + fn device_id(&self) -> Result { Ok(self.upgrade_user()?.user_config.device_id.clone()) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 1bd3223946..56ac310300 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -1,4 +1,4 @@ -use af_local_ai::ai_ops::{LocalAITranslateItem, LocalAITranslateRowData}; +use appflowy_local_ai::ai_ops::{LocalAITranslateItem, LocalAITranslateRowData}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_ai::ai_manager::AIManager; @@ -13,7 +13,6 @@ use lib_infra::async_trait::async_trait; use lib_infra::priority_task::TaskDispatcher; use std::sync::{Arc, Weak}; use tokio::sync::RwLock; -use uuid::Uuid; pub struct DatabaseDepsResolver(); @@ -48,46 +47,46 @@ struct DatabaseAIServiceMiddleware { impl DatabaseAIService for DatabaseAIServiceMiddleware { async fn summary_database_row( &self, - workspace_id: &Uuid, - object_id: &Uuid, - _summary_row: SummaryRowContent, + workspace_id: &str, + object_id: &str, + summary_row: SummaryRowContent, ) -> Result { - if self.ai_manager.local_ai.is_running() { + if self.ai_manager.local_ai_controller.is_running() { self .ai_manager - .local_ai - .summary_database_row(_summary_row) + .local_ai_controller + .summary_database_row(summary_row) .await .map_err(|err| FlowyError::local_ai().with_context(err)) } else { self .ai_service - .summary_database_row(workspace_id, object_id, _summary_row) + .summary_database_row(workspace_id, object_id, summary_row) .await } } async fn translate_database_row( &self, - _workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, ) -> Result { - if self.ai_manager.local_ai.is_running() { + if self.ai_manager.local_ai_controller.is_running() { let data = LocalAITranslateRowData { - cells: _translate_row + cells: translate_row .into_iter() .map(|row| LocalAITranslateItem { title: row.title, content: row.content, }) .collect(), - language: _language.to_string(), + language: language.to_string(), include_header: false, }; let resp = self .ai_manager - .local_ai + .local_ai_controller .translate_database_row(data) .await .map_err(|err| FlowyError::local_ai().with_context(err))?; @@ -96,7 +95,7 @@ impl DatabaseAIService for DatabaseAIServiceMiddleware { } else { self .ai_service - .translate_database_row(_workspace_id, _translate_row, _language) + .translate_database_row(workspace_id, translate_row, language) .await } } @@ -122,11 +121,11 @@ impl DatabaseUser for DatabaseUserImpl { self.upgrade_user()?.get_collab_db(uid) } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } - fn workspace_database_object_id(&self) -> Result { + fn workspace_database_object_id(&self) -> Result { self.upgrade_user()?.workspace_database_object_id() } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index 3527bc42d6..a4203d8268 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Weak}; + use crate::deps_resolve::CollabSnapshotSql; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; @@ -8,8 +10,6 @@ use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{FlowyError, FlowyResult}; use flowy_storage_pub::storage::StorageService; use flowy_user::services::authenticate_user::AuthenticateUser; -use std::sync::{Arc, Weak}; -use uuid::Uuid; pub struct DocumentDepsResolver(); impl DocumentDepsResolver { @@ -97,7 +97,7 @@ impl DocumentUserService for DocumentUserImpl { .device_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self .0 .upgrade() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs index bee5f19ced..f0e6985a78 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs @@ -4,7 +4,6 @@ use flowy_storage::manager::{StorageManager, StorageUserService}; use flowy_storage_pub::cloud::StorageCloudService; use flowy_user::services::authenticate_user::AuthenticateUser; use std::sync::{Arc, Weak}; -use uuid::Uuid; pub struct FileStorageResolver; @@ -41,7 +40,7 @@ impl StorageUserService for FileStorageServiceImpl { self.upgrade_user()?.user_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs index e2791827ee..40a7657967 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs @@ -8,7 +8,6 @@ use flowy_folder::share::ImportType; use flowy_folder::view_operation::{FolderOperationHandler, ImportedData}; use lib_infra::async_trait::async_trait; use std::sync::Arc; -use uuid::Uuid; pub struct ChatFolderOperation(pub Arc); @@ -18,20 +17,20 @@ impl FolderOperationHandler for ChatFolderOperation { "ChatFolderOperationHandler" } - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.open_chat(view_id).await } - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.close_chat(view_id).await } - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.delete_chat(view_id).await } - async fn duplicate_view(&self, _view_id: &Uuid) -> Result { - Err(FlowyError::not_support().with_context("Duplicate view")) + async fn duplicate_view(&self, _view_id: &str) -> Result { + Err(FlowyError::not_support()) } async fn create_view_with_view_data( @@ -39,14 +38,14 @@ impl FolderOperationHandler for ChatFolderOperation { _user_id: i64, _params: CreateViewParams, ) -> Result, FlowyError> { - Err(FlowyError::not_support().with_context("Can't create view")) + Err(FlowyError::not_support()) } async fn create_default_view( &self, user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, + parent_view_id: &str, + view_id: &str, _name: &str, _layout: ViewLayout, ) -> Result<(), FlowyError> { @@ -60,12 +59,12 @@ impl FolderOperationHandler for ChatFolderOperation { async fn import_from_bytes( &self, _uid: i64, - _view_id: &Uuid, + _view_id: &str, _name: &str, _import_type: ImportType, _bytes: Vec, ) -> Result, FlowyError> { - Err(FlowyError::not_support().with_context("import from data")) + Err(FlowyError::not_support()) } async fn import_from_file_path( @@ -74,6 +73,6 @@ impl FolderOperationHandler for ChatFolderOperation { _name: &str, _path: String, ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("import file from path")) + Err(FlowyError::not_support()) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs index edc40c6d5b..d7d0c4d0cc 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs @@ -1,4 +1,3 @@ -#![allow(unused_variables)] use bytes::Bytes; use collab::entity::EncodedCollab; use collab_entity::CollabType; @@ -20,31 +19,23 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; -use uuid::Uuid; pub struct DatabaseFolderOperation(pub Arc); #[async_trait] impl FolderOperationHandler for DatabaseFolderOperation { - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.open_database_view(view_id).await?; Ok(()) } - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self - .0 - .close_database_view(view_id.to_string().as_str()) - .await?; + async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { + self.0.close_database_view(view_id).await?; Ok(()) } - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - match self - .0 - .delete_database_view(view_id.to_string().as_str()) - .await - { + async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { + match self.0.delete_database_view(view_id).await { Ok(_) => tracing::trace!("Delete database view: {}", view_id), Err(e) => tracing::error!("🔴delete database failed: {}", e), } @@ -53,20 +44,16 @@ impl FolderOperationHandler for DatabaseFolderOperation { async fn gather_publish_encode_collab( &self, - _user: &Arc, - view_id: &Uuid, + user: &Arc, + view_id: &str, ) -> Result { - let workspace_id = _user.workspace_id()?; - let view_id_str = view_id.to_string(); + let workspace_id = user.workspace_id()?; // get the collab_object_id for the database. // // the collab object_id for the database is not the view_id, // we should use the view_id to get the database_id - let oid = self.0.get_database_id_with_view_id(&view_id_str).await?; - let row_oids = self - .0 - .get_database_row_ids_with_view_id(&view_id_str) - .await?; + let oid = self.0.get_database_id_with_view_id(view_id).await?; + let row_oids = self.0.get_database_row_ids_with_view_id(view_id).await?; let row_metas = self .0 .get_database_row_metas_with_view_id(view_id, row_oids.clone()) @@ -81,12 +68,12 @@ impl FolderOperationHandler for DatabaseFolderOperation { .collect::>(); let database_metas = self.0.get_all_databases_meta().await; - let uid = _user + let uid = user .user_id() .map_err(|e| e.with_context("unable to get the uid: {}"))?; // get the collab db - let collab_db = _user + let collab_db = user .collab_db(uid) .map_err(|e| e.with_context("unable to get the collab"))?; let collab_db = collab_db.upgrade().ok_or_else(|| { @@ -97,7 +84,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { tokio::task::spawn_blocking(move || { let collab_read_txn = collab_db.read_txn(); - let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), &oid) + let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, &oid) .map_err(|e| { FlowyError::internal().with_context(format!("load database collab failed: {}", e)) })?; @@ -110,7 +97,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { })?; let database_row_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_oids) + load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_oids) .0 .into_iter() .map(|(oid, collab)| { @@ -136,7 +123,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { .collect::>(); let database_row_document_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_document_ids) + load_collab_by_object_ids(uid, &workspace_id, &collab_read_txn, &row_document_ids) .0 .into_iter() .map(|(oid, collab)| { @@ -160,7 +147,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { .await? } - async fn duplicate_view(&self, view_id: &Uuid) -> Result { + async fn duplicate_view(&self, view_id: &str) -> Result { Ok(Bytes::from(view_id.to_string())) } @@ -179,14 +166,14 @@ impl FolderOperationHandler for DatabaseFolderOperation { String::from_utf8(data.to_vec()).map_err(|_| FlowyError::invalid_data())?; let encoded_collab = self .0 - .duplicate_database(&duplicated_view_id, ¶ms.view_id.to_string()) + .duplicate_database(&duplicated_view_id, ¶ms.view_id) .await?; Ok(Some(encoded_collab)) }, ViewData::Data(data) => { let encoded_collab = self .0 - .create_database_with_data(¶ms.view_id.to_string(), data.to_vec()) + .create_database_with_data(¶ms.view_id, data.to_vec()) .await?; Ok(Some(encoded_collab)) }, @@ -198,9 +185,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar, ViewLayoutPB::Grid => DatabaseLayoutPB::Grid, ViewLayoutPB::Document | ViewLayoutPB::Chat => { - return Err( - FlowyError::invalid_data().with_context("Can't handle document layout type"), - ); + return Err(FlowyError::not_support()); }, }; let name = params.name.to_string(); @@ -227,18 +212,17 @@ impl FolderOperationHandler for DatabaseFolderOperation { /// these references views. async fn create_default_view( &self, - user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, + _user_id: i64, + _parent_view_id: &str, + view_id: &str, name: &str, layout: ViewLayout, ) -> Result<(), FlowyError> { let name = name.to_string(); - let view_id = view_id.to_string(); let data = match layout { - ViewLayout::Grid => make_default_grid(&view_id, &name), - ViewLayout::Board => make_default_board(&view_id, &name), - ViewLayout::Calendar => make_default_calendar(&view_id, &name), + ViewLayout::Grid => make_default_grid(view_id, &name), + ViewLayout::Board => make_default_board(view_id, &name), + ViewLayout::Calendar => make_default_calendar(view_id, &name), ViewLayout::Document | ViewLayout::Chat => { return Err( FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)), @@ -260,9 +244,9 @@ impl FolderOperationHandler for DatabaseFolderOperation { async fn import_from_bytes( &self, - uid: i64, - view_id: &Uuid, - name: &str, + _uid: i64, + view_id: &str, + _name: &str, import_type: ImportType, bytes: Vec, ) -> Result, FlowyError> { diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs index a843a8eb1f..af95b8987d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs @@ -17,10 +17,8 @@ use flowy_folder::view_operation::{ use lib_dispatch::prelude::ToBytes; use lib_infra::async_trait::async_trait; use std::convert::TryFrom; -use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; -use uuid::Uuid; pub struct DocumentFolderOperation(pub Arc); #[async_trait] @@ -35,7 +33,6 @@ impl FolderOperationHandler for DocumentFolderOperation { workspace_view_builder: Arc>, ) -> Result<(), FlowyError> { let manager = self.0.clone(); - let mut write_guard = workspace_view_builder.write().await; // Create a view named "Getting started" with an icon ⭐️ and the built-in README data. // Don't modify this code unless you know what you are doing. @@ -48,9 +45,8 @@ impl FolderOperationHandler for DocumentFolderOperation { // create a empty document let json_str = include_str!("../../../assets/read_me.json"); let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); - let view_id = Uuid::from_str(&view.view.id).unwrap(); manager - .create_document(uid, &view_id, Some(document_pb.into())) + .create_document(uid, &view.view.id, Some(document_pb.into())) .await .unwrap(); view @@ -59,18 +55,18 @@ impl FolderOperationHandler for DocumentFolderOperation { Ok(()) } - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn open_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.open_document(view_id).await?; Ok(()) } /// Close the document view. - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { self.0.close_document(view_id).await?; Ok(()) } - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { + async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError> { match self.0.delete_document(view_id).await { Ok(_) => tracing::trace!("Delete document: {}", view_id), Err(e) => tracing::error!("🔴delete document failed: {}", e), @@ -78,7 +74,7 @@ impl FolderOperationHandler for DocumentFolderOperation { Ok(()) } - async fn duplicate_view(&self, view_id: &Uuid) -> Result { + async fn duplicate_view(&self, view_id: &str) -> Result { let data: DocumentDataPB = self.0.get_document_data(view_id).await?.into(); let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; Ok(data_bytes) @@ -87,11 +83,10 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn gather_publish_encode_collab( &self, user: &Arc, - view_id: &Uuid, + view_id: &str, ) -> Result { let encoded_collab = - get_encoded_collab_v1_from_disk(user, view_id.to_string().as_str(), CollabType::Document) - .await?; + get_encoded_collab_v1_from_disk(user, view_id, CollabType::Document).await?; Ok(GatherEncodedCollab::Document(encoded_collab)) } @@ -117,8 +112,8 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn create_default_view( &self, user_id: i64, - _parent_view_id: &Uuid, - view_id: &Uuid, + _parent_view_id: &str, + view_id: &str, _name: &str, layout: ViewLayout, ) -> Result<(), FlowyError> { @@ -138,7 +133,7 @@ impl FolderOperationHandler for DocumentFolderOperation { async fn import_from_bytes( &self, uid: i64, - view_id: &Uuid, + view_id: &str, _name: &str, _import_type: ImportType, bytes: Vec, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs index 02b26e71b6..9bed61d918 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs @@ -17,7 +17,6 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_sqlite::kv::KVStorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::services::data_import::load_collab_by_object_id; -use std::str::FromStr; use std::sync::{Arc, Weak}; use crate::deps_resolve::folder_deps::folder_deps_chat_impl::ChatFolderOperation; @@ -26,7 +25,6 @@ use crate::deps_resolve::folder_deps::folder_deps_doc_impl::DocumentFolderOperat use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_folder_pub::query::{FolderQueryService, FolderService, FolderViewEdit, QueryCollab}; use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub struct FolderDepsResolver(); #[allow(clippy::too_many_arguments)] @@ -91,7 +89,7 @@ impl FolderUser for FolderUserImpl { self.upgrade_user()?.user_id() } - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { self.upgrade_user()?.workspace_id() } @@ -99,10 +97,8 @@ impl FolderUser for FolderUserImpl { self.upgrade_user()?.get_collab_db(uid) } - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult { - self - .upgrade_user()? - .is_collab_on_disk(uid, workspace_id.to_string().as_str()) + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult { + self.upgrade_user()?.is_collab_on_disk(uid, workspace_id) } } @@ -128,13 +124,13 @@ impl FolderServiceImpl { #[async_trait] impl FolderViewEdit for FolderServiceImpl { - async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()> { + async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()> { if title.is_empty() { return Ok(()); } if let Some(folder_manager) = self.folder_manager.upgrade() { - if let Ok(view) = folder_manager.get_view(view_id.to_string().as_str()).await { + if let Ok(view) = folder_manager.get_view(view_id).await { if view.name.is_empty() { let title = if title.len() > 50 { title.chars().take(50).collect() @@ -164,25 +160,22 @@ impl FolderViewEdit for FolderServiceImpl { impl FolderQueryService for FolderServiceImpl { async fn get_surrounding_view_ids_with_view_layout( &self, - parent_view_id: &Uuid, + parent_view_id: &str, view_layout: ViewLayout, - ) -> Vec { + ) -> Vec { let folder_manager = match self.folder_manager.upgrade() { Some(folder_manager) => folder_manager, None => return vec![], }; - if let Ok(view) = folder_manager - .get_view(parent_view_id.to_string().as_str()) - .await - { + if let Ok(view) = folder_manager.get_view(parent_view_id).await { if view.space_info().is_some() { return vec![]; } } match folder_manager - .get_untrashed_views_belong_to(parent_view_id.to_string().as_str()) + .get_untrashed_views_belong_to(parent_view_id) .await { Ok(views) => { @@ -190,24 +183,23 @@ impl FolderQueryService for FolderServiceImpl { .into_iter() .filter_map(|child| { if child.layout == view_layout { - Uuid::from_str(&child.id).ok() + Some(child.id.clone()) } else { None } }) .collect::>(); - children.push(*parent_view_id); + children.push(parent_view_id.to_string()); children }, _ => vec![], } } - async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option { - let encode_collab = - get_encoded_collab_v1_from_disk(&self.user, object_id.to_string().as_str(), collab_type) - .await - .ok(); + async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option { + let encode_collab = get_encoded_collab_v1_from_disk(&self.user, object_id, collab_type.clone()) + .await + .ok(); encode_collab.map(|encoded_collab| QueryCollab { collab_type, @@ -237,8 +229,8 @@ async fn get_encoded_collab_v1_from_disk( ) })?; let collab_read_txn = collab_db.read_txn(); - let collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), view_id) - .map_err(|e| { + let collab = + load_collab_by_object_id(uid, &collab_read_txn, &workspace_id, view_id).map_err(|e| { FlowyError::internal().with_context(format!("load document collab failed: {}", e)) })?; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs index 73c2844a23..b6179a1ad8 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -13,7 +13,6 @@ use lib_infra::async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; use tracing::info; -use uuid::Uuid; pub struct UserDepsResolver(); @@ -82,13 +81,12 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl { Ok(()) } - async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()> { // The remove_indices_for_workspace should not block the deletion of the workspace // Log the error and continue if let Err(err) = self .folder_manager .remove_indices_for_workspace(workspace_id) - .await { info!("Error removing indices for workspace: {}", err); } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c2800bd73b..4c84e73c08 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,22 +1,20 @@ #![allow(unused_doc_comments)] -use collab_integrate::collab_builder::AppFlowyCollabBuilder; -use collab_plugins::CollabKVDB; -use flowy_ai::ai_manager::AIManager; -use flowy_database2::DatabaseManager; -use flowy_document::manager::DocumentManager; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::LoggedUser; -use std::path::PathBuf; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; use tokio::sync::RwLock; use tracing::{debug, error, event, info, instrument}; -use uuid::Uuid; + +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; +use flowy_ai::ai_manager::AIManager; +use flowy_database2::DatabaseManager; +use flowy_document::manager::DocumentManager; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_folder::manager::FolderManager; +use flowy_server::af_cloud::define::ServerUser; use flowy_sqlite::kv::KVStorePreferences; use flowy_storage::manager::StorageManager; @@ -35,10 +33,8 @@ use crate::config::AppFlowyCoreConfig; use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; use crate::log_filter::init_log; -use crate::server_layer::ServerProvider; +use crate::server_layer::{current_server_type, Server, ServerProvider}; use deps_resolve::reminder_deps::CollabInteractImpl; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; use user_state_callback::UserStatusCallbackImpl; pub mod config; @@ -109,8 +105,6 @@ impl AppFlowyCore { #[instrument(skip(config, runtime))] async fn init(config: AppFlowyCoreConfig, runtime: Arc) -> Self { - config.ensure_path(); - // Init the key value database let store_preference = Arc::new(KVStorePreferences::new(&config.storage_path).unwrap()); info!("🔥{:?}", &config); @@ -132,10 +126,12 @@ impl AppFlowyCore { store_preference.clone(), )); - debug!("🔥runtime:{}", runtime); + let server_type = current_server_type(); + debug!("🔥runtime:{}, server:{}", runtime, server_type); let server_provider = Arc::new(ServerProvider::new( config.clone(), + server_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -167,9 +163,9 @@ impl AppFlowyCore { collab_builder .set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&authenticate_user)))); - let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade( + let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade( &authenticate_user, - ))); + )))); let folder_manager = FolderDepsResolver::resolve( Arc::downgrade(&authenticate_user), @@ -192,7 +188,6 @@ impl AppFlowyCore { Arc::downgrade(&storage_manager.storage_service), server_provider.clone(), folder_query_service.clone(), - server_provider.local_ai.clone(), ); let database_manager = DatabaseDepsResolver::resolve( @@ -253,7 +248,6 @@ impl AppFlowyCore { .await; let user_status_callback = UserStatusCallbackImpl { - user_manager: user_manager.clone(), collab_builder, folder_manager: folder_manager.clone(), database_manager: database_manager.clone(), @@ -261,7 +255,6 @@ impl AppFlowyCore { server_provider: server_provider.clone(), storage_manager: storage_manager.clone(), ai_manager: ai_manager.clone(), - runtime: runtime.clone(), }; let collab_interact_impl = CollabInteractImpl { @@ -314,6 +307,15 @@ impl AppFlowyCore { } } +impl From for CollabPluginProviderType { + fn from(server_type: Server) -> Self { + match server_type { + Server::Local => CollabPluginProviderType::Local, + Server::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + } + } +} + struct ServerUserImpl(Weak); impl ServerUserImpl { @@ -325,32 +327,8 @@ impl ServerUserImpl { Ok(user) } } - -#[async_trait] -impl LoggedUser for ServerUserImpl { - fn workspace_id(&self) -> FlowyResult { +impl ServerUser for ServerUserImpl { + fn workspace_id(&self) -> FlowyResult { self.upgrade_user()?.workspace_id() } - - fn user_id(&self) -> FlowyResult { - self.upgrade_user()?.user_id() - } - - async fn is_local_mode(&self) -> FlowyResult { - self.upgrade_user()?.is_local_mode().await - } - - fn get_sqlite_db(&self, uid: i64) -> Result { - self.upgrade_user()?.get_sqlite_connection(uid) - } - - fn get_collab_db(&self, uid: i64) -> Result, FlowyError> { - self.upgrade_user()?.get_collab_db(uid) - } - - fn application_root_dir(&self) -> Result { - Ok(PathBuf::from( - self.upgrade_user()?.get_application_root_dir(), - )) - } } diff --git a/frontend/rust-lib/flowy-core/src/log_filter.rs b/frontend/rust-lib/flowy-core/src/log_filter.rs index 6704ad0507..63877862f0 100644 --- a/frontend/rust-lib/flowy-core/src/log_filter.rs +++ b/frontend/rust-lib/flowy-core/src/log_filter.rs @@ -57,8 +57,8 @@ pub fn create_log_filter( filters.push(format!("lib_infra={}", level)); filters.push(format!("flowy_search={}", level)); filters.push(format!("flowy_chat={}", level)); - filters.push(format!("af_local_ai={}", level)); - filters.push(format!("af_plugin={}", level)); + filters.push(format!("appflowy_local_ai={}", level)); + filters.push(format!("appflowy_plugin={}", level)); filters.push(format!("flowy_ai={}", level)); filters.push(format!("flowy_storage={}", level)); // Enable the frontend logs. DO NOT DISABLE. diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs index 6e5d35d726..0d304c6063 100644 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ b/frontend/rust-lib/flowy-core/src/server_layer.rs @@ -1,134 +1,194 @@ -use crate::AppFlowyCoreConfig; -use af_plugin::manager::PluginManager; -use arc_swap::{ArcSwap, ArcSwapOption}; -use dashmap::mapref::one::Ref; +use arc_swap::ArcSwapOption; use dashmap::DashMap; -use flowy_ai::local_ai::controller::LocalAIController; +use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::{Arc, Weak}; + +use serde_repr::*; + use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::AIUserServiceImpl; -use flowy_server::af_cloud::{define::LoggedUser, AppFlowyCloudServer}; -use flowy_server::local_server::LocalServer; +use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::local_server::{LocalServer, LocalServerDB}; use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::entities::*; -use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; -use tracing::info; +use crate::AppFlowyCoreConfig; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum Server { + /// Local server provider. + /// Offline mode, no user authentication and the data is stored locally. + Local = 0, + /// AppFlowy Cloud server provider. + /// See: https://github.com/AppFlowy-IO/AppFlowy-Cloud + AppFlowyCloud = 1, +} + +impl Server { + pub fn is_local(&self) -> bool { + matches!(self, Server::Local) + } +} + +impl Display for Server { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Server::Local => write!(f, "Local"), + Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), + } + } +} + +/// The [ServerProvider] provides list of [AppFlowyServer] base on the [Authenticator]. Using +/// the auth type, the [ServerProvider] will create a new [AppFlowyServer] if it doesn't +/// exist. +/// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. pub struct ServerProvider { config: AppFlowyCoreConfig, - providers: DashMap>, - auth_type: ArcSwap, - logged_user: Arc, - pub local_ai: Arc, - pub uid: Arc>, - pub user_enable_sync: Arc, - pub encryption: Arc, -} + providers: DashMap>, + pub(crate) encryption: Arc, + #[allow(dead_code)] + pub(crate) store_preferences: Weak, + pub(crate) user_enable_sync: AtomicBool, -// Our little guard wrapper: -pub struct ServerHandle<'a>(Ref<'a, AuthType, Arc>); - -impl<'a> Deref for ServerHandle<'a> { - type Target = dyn AppFlowyServer; - fn deref(&self) -> &Self::Target { - // `self.0.value()` is an `&Arc` - // so `&**` gives us a `&dyn AppFlowyServer` - &**self.0.value() - } -} - -/// Determine current server type from ENV -pub fn current_server_type() -> AuthType { - match AuthenticatorType::from_env() { - AuthenticatorType::Local => AuthType::Local, - AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, - } + /// The authenticator type of the user. + authenticator: AtomicU8, + user: Arc, + pub(crate) uid: Arc>, } impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, + server: Server, store_preferences: Weak, - user_service: impl LoggedUser + 'static, + server_user: impl ServerUser + 'static, ) -> Self { - let initial_auth = current_server_type(); - let logged_user = Arc::new(user_service) as Arc; - let auth_type = ArcSwap::from(Arc::new(initial_auth)); - let encryption = Arc::new(EncryptionImpl::new(None)) as Arc; - let ai_user = Arc::new(AIUserServiceImpl(Arc::downgrade(&logged_user))); - let plugins = Arc::new(PluginManager::new()); - let local_ai = Arc::new(LocalAIController::new( - plugins, - store_preferences, - ai_user.clone(), - )); - - ServerProvider { + let user = Arc::new(server_user); + let encryption = EncryptionImpl::new(None); + Self { config, providers: DashMap::new(), - encryption, - user_enable_sync: Arc::new(AtomicBool::new(true)), - auth_type, - logged_user, + user_enable_sync: AtomicBool::new(true), + authenticator: AtomicU8::new(Authenticator::from(server) as u8), + encryption: Arc::new(encryption), + store_preferences, uid: Default::default(), - local_ai, + user, } } - pub fn set_auth_type(&self, new_auth_type: AuthType) { - let old_type = self.get_auth_type(); - if old_type != new_auth_type { - info!( - "ServerProvider: auth type from {:?} to {:?}", - old_type, new_auth_type - ); - - self.auth_type.store(Arc::new(new_auth_type)); - if let Some((auth_type, _)) = self.providers.remove(&old_type) { - info!("ServerProvider: remove old auth type: {:?}", auth_type); - } + pub fn get_server_type(&self) -> Server { + match Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) { + Authenticator::Local => Server::Local, + Authenticator::AppFlowyCloud => Server::AppFlowyCloud, } } - pub fn get_auth_type(&self) -> AuthType { - *self.auth_type.load_full().as_ref() + pub fn set_authenticator(&self, authenticator: Authenticator) { + let old_server_type = self.get_server_type(); + self + .authenticator + .store(authenticator as u8, Ordering::Release); + let new_server_type = self.get_server_type(); + + if old_server_type != new_server_type { + self.providers.remove(&old_server_type); + } } - /// Lazily create or fetch an AppFlowyServer instance - pub fn get_server(&self) -> FlowyResult { - let auth_type = self.get_auth_type(); - if let Some(r) = self.providers.get(&auth_type) { - return Ok(ServerHandle(r)); + pub fn get_authenticator(&self) -> Authenticator { + Authenticator::from(self.authenticator.load(Ordering::Acquire) as i32) + } + + /// Returns a [AppFlowyServer] trait implementation base on the provider_type. + pub fn get_server(&self) -> FlowyResult> { + let server_type = self.get_server_type(); + + if let Some(provider) = self.providers.get(&server_type) { + return Ok(provider.value().clone()); } - let server: Arc = match auth_type { - AuthType::Local => Arc::new(LocalServer::new( - self.logged_user.clone(), - self.local_ai.clone(), - )), - AuthType::AppFlowyCloud => { - let cfg = self - .config - .cloud_config - .clone() - .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?; - let ai_user_service = Arc::new(AIUserServiceImpl(Arc::downgrade(&self.logged_user))); - Arc::new(AppFlowyCloudServer::new( - cfg, + let server = match server_type { + Server::Local => { + let local_db = Arc::new(LocalServerDBImpl { + storage_path: self.config.storage_path.clone(), + }); + let server = Arc::new(LocalServer::new(local_db)); + Ok::, FlowyError>(server) + }, + Server::AppFlowyCloud => { + let config = self.config.cloud_config.clone().ok_or_else(|| { + FlowyError::internal().with_context("AppFlowyCloud configuration is missing") + })?; + let server = Arc::new(AppFlowyCloudServer::new( + config, self.user_enable_sync.load(Ordering::Acquire), self.config.device_id.clone(), self.config.app_version.clone(), - Arc::downgrade(&self.logged_user), - ai_user_service, - )) - }, - }; + self.user.clone(), + )); - self.providers.insert(auth_type, server); - let guard = self.providers.get(&auth_type).unwrap(); - Ok(ServerHandle(guard)) + Ok::, FlowyError>(server) + }, + }?; + + self.providers.insert(server_type.clone(), server.clone()); + Ok(server) + } +} + +impl From for Server { + fn from(auth_provider: Authenticator) -> Self { + match auth_provider { + Authenticator::Local => Server::Local, + Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + } + } +} + +impl From for Authenticator { + fn from(ty: Server) -> Self { + match ty { + Server::Local => Authenticator::Local, + Server::AppFlowyCloud => Authenticator::AppFlowyCloud, + } + } +} +impl From<&Authenticator> for Server { + fn from(auth_provider: &Authenticator) -> Self { + Self::from(auth_provider.clone()) + } +} + +pub fn current_server_type() -> Server { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => Server::Local, + AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, + } +} + +struct LocalServerDBImpl { + #[allow(dead_code)] + storage_path: String, +} + +impl LocalServerDB for LocalServerDBImpl { + fn get_user_profile(&self, _uid: i64) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("LocalServer doesn't support get_user_profile"), + ) + } + + fn get_user_workspace(&self, _uid: i64) -> Result, FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("LocalServer doesn't support get_user_workspace"), + ) } } diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs index f191d3c1ad..a43c28f90b 100644 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ b/frontend/rust-lib/flowy-core/src/user_state_callback.rs @@ -2,29 +2,24 @@ use std::sync::Arc; use anyhow::Context; use client_api::entity::billing_dto::SubscriptionPlan; -use tracing::{error, event, info}; +use tracing::{event, info}; -use crate::server_layer::ServerProvider; use collab_entity::CollabType; use collab_integrate::collab_builder::AppFlowyCollabBuilder; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyResult; use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_storage::manager::StorageManager; use flowy_user::event_map::UserStatusCallback; -use flowy_user::user_manager::UserManager; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; -use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; -use lib_dispatch::runtime::AFPluginRuntime; +use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; use lib_infra::async_trait::async_trait; -use uuid::Uuid; + +use crate::server_layer::{Server, ServerProvider}; pub(crate) struct UserStatusCallbackImpl { - pub(crate) user_manager: Arc, pub(crate) collab_builder: Arc, pub(crate) folder_manager: Arc, pub(crate) database_manager: Arc, @@ -32,60 +27,23 @@ pub(crate) struct UserStatusCallbackImpl { pub(crate) server_provider: Arc, pub(crate) storage_manager: Arc, pub(crate) ai_manager: Arc, - // By default, all callback will run on the caller thread. If you don't want to block the caller - // thread, you can use runtime to spawn a new task. - pub(crate) runtime: Arc, -} - -impl UserStatusCallbackImpl { - async fn folder_init_data_source( - &self, - user_id: i64, - workspace_id: &Uuid, - auth_type: &AuthType, - ) -> FlowyResult { - if self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)? { - return Ok(FolderInitDataSource::LocalDisk { - create_if_not_exist: false, - }); - } - let doc_state_result = self - .folder_manager - .cloud_service - .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) - .await; - resolve_data_source(auth_type, doc_state_result) - } - - fn is_object_exist_on_disk( - &self, - user_id: i64, - workspace_id: &Uuid, - object_id: &Uuid, - ) -> FlowyResult { - let db = self - .user_manager - .get_collab_db(user_id)? - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Collab db is not initialized"))?; - let read = db.read_txn(); - let workspace_id = workspace_id.to_string(); - let object_id = object_id.to_string(); - Ok(read.is_exist(user_id, &workspace_id, &object_id)) - } } #[async_trait] impl UserStatusCallback for UserStatusCallbackImpl { - async fn on_launch_if_authenticated( + async fn did_init( &self, user_id: i64, + user_authenticator: &Authenticator, cloud_config: &Option, user_workspace: &UserWorkspace, _device_id: &str, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { - let workspace_id = user_workspace.workspace_id()?; + self + .server_provider + .set_user_authenticator(user_authenticator); + if let Some(cloud_config) = cloud_config { self .server_provider @@ -101,7 +59,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .folder_manager .initialize( user_id, - &workspace_id, + &user_workspace.id, FolderInitDataSource::LocalDisk { create_if_not_exist: false, }, @@ -109,29 +67,19 @@ impl UserStatusCallback for UserStatusCallbackImpl { .await?; self .database_manager - .initialize(user_id, auth_type == &AuthType::Local) + .initialize(user_id, authenticator == &Authenticator::Local) .await?; self.document_manager.initialize(user_id).await?; - - let workspace_id = user_workspace.id.clone(); - let cloned_ai_manager = self.ai_manager.clone(); - self.runtime.spawn(async move { - if let Err(err) = cloned_ai_manager - .on_launch_if_authenticated(&workspace_id) - .await - { - error!("Failed to initialize AIManager: {:?}", err); - } - }); + self.ai_manager.initialize(&user_workspace.id).await?; Ok(()) } - async fn on_sign_in( + async fn did_sign_in( &self, user_id: i64, user_workspace: &UserWorkspace, device_id: &str, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { event!( tracing::Level::TRACE, @@ -139,39 +87,32 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); - let workspace_id = user_workspace.workspace_id()?; - let data_source = self - .folder_init_data_source(user_id, &workspace_id, auth_type) - .await?; + self .folder_manager - .initialize_after_sign_in(user_id, data_source) + .initialize_with_workspace_id(user_id) .await?; self .database_manager - .initialize_after_sign_in(user_id, auth_type.is_local()) + .initialize(user_id, authenticator.is_local()) .await?; - self - .document_manager - .initialize_after_sign_in(user_id) - .await?; - - self - .ai_manager - .initialize_after_sign_in(&user_workspace.id) - .await?; - + self.document_manager.initialize(user_id).await?; Ok(()) } - async fn on_sign_up( + async fn did_sign_up( &self, is_new_user: bool, user_profile: &UserProfile, user_workspace: &UserWorkspace, device_id: &str, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { + self + .server_provider + .set_user_authenticator(&user_profile.authenticator); + let server_type = self.server_provider.get_server_type(); + event!( tracing::Level::TRACE, "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", @@ -179,88 +120,96 @@ impl UserStatusCallback for UserStatusCallbackImpl { user_workspace, device_id ); - let workspace_id = user_workspace.workspace_id()?; - let data_source = self - .folder_init_data_source(user_profile.uid, &workspace_id, auth_type) - .await?; + + // In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace + // is automatically created for them. However, for users who sign up through Supabase, the creation + // of the default workspace relies on the client-side operation. This means that the process + // for initializing a default workspace differs depending on the sign-up method used. + let data_source = match self + .folder_manager + .cloud_service + .get_folder_doc_state( + &user_workspace.id, + user_profile.uid, + CollabType::Folder, + &user_workspace.id, + ) + .await + { + Ok(doc_state) => match server_type { + Server::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + Server::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + }, + Err(err) => match server_type { + Server::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + Server::AppFlowyCloud => { + return Err(err); + }, + }, + }; self .folder_manager - .initialize_after_sign_up( + .initialize_with_new_user( user_profile.uid, &user_profile.token, is_new_user, data_source, - &workspace_id, + &user_workspace.id, ) .await .context("FolderManager error")?; self .database_manager - .initialize_after_sign_up(user_profile.uid, auth_type.is_local()) + .initialize_with_new_user(user_profile.uid, authenticator.is_local()) .await .context("DatabaseManager error")?; self .document_manager - .initialize_after_sign_up(user_profile.uid) + .initialize_with_new_user(user_profile.uid) .await .context("DocumentManager error")?; - - self - .ai_manager - .initialize_after_sign_up(&user_workspace.id) - .await?; Ok(()) } - async fn on_token_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { + async fn did_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { self.folder_manager.clear(user_id).await; Ok(()) } - async fn on_workspace_opened( + async fn open_workspace( &self, user_id: i64, - workspace_id: &Uuid, - _user_workspace: &UserWorkspace, - auth_type: &AuthType, + user_workspace: &UserWorkspace, + authenticator: &Authenticator, ) -> FlowyResult<()> { - let data_source = self - .folder_init_data_source(user_id, workspace_id, auth_type) - .await?; - self .folder_manager - .initialize_after_open_workspace(user_id, data_source) + .initialize_with_workspace_id(user_id) .await?; self .database_manager - .initialize_after_open_workspace(user_id, auth_type.is_local()) + .initialize(user_id, authenticator.is_local()) .await?; - self - .document_manager - .initialize_after_open_workspace(user_id) - .await?; - self - .ai_manager - .initialize_after_open_workspace(workspace_id) - .await?; - self - .storage_manager - .initialize_after_open_workspace(workspace_id) - .await; + self.document_manager.initialize(user_id).await?; + self.ai_manager.initialize(&user_workspace.id).await?; + self.storage_manager.initialize(&user_workspace.id).await; Ok(()) } - fn on_network_status_changed(&self, reachable: bool) { + fn did_update_network(&self, reachable: bool) { info!("Notify did update network: reachable: {}", reachable); self.collab_builder.update_network(reachable); self.storage_manager.update_network_reachable(reachable); } - fn on_subscription_plans_updated(&self, plans: Vec) { + fn did_update_plans(&self, plans: Vec) { let mut storage_plan_changed = false; for plan in &plans { match plan { @@ -273,7 +222,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } - fn on_storage_permission_updated(&self, can_write: bool) { + fn did_update_storage_limitation(&self, can_write: bool) { if can_write { self.storage_manager.enable_storage_write_access(); } else { @@ -281,23 +230,3 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } } - -fn resolve_data_source( - auth_type: &AuthType, - doc_state_result: Result, FlowyError>, -) -> FlowyResult { - match doc_state_result { - Ok(doc_state) => Ok(match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }), - Err(err) => match auth_type { - AuthType::Local => Ok(FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }), - AuthType::AppFlowyCloud => Err(err), - }, - } -} diff --git a/frontend/rust-lib/flowy-database-pub/Cargo.toml b/frontend/rust-lib/flowy-database-pub/Cargo.toml index 088c7b6465..91426a5c87 100644 --- a/frontend/rust-lib/flowy-database-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-database-pub/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" lib-infra = { workspace = true } collab-entity = { workspace = true } collab = { workspace = true } +anyhow.workspace = true client-api = { workspace = true } -flowy-error = { workspace = true } -uuid.workspace = true \ No newline at end of file +flowy-error = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 8666e6c764..a29cf650c4 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -4,9 +4,8 @@ use collab_entity::CollabType; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; use std::collections::HashMap; -use uuid::Uuid; -pub type EncodeCollabByOid = HashMap; +pub type EncodeCollabByOid = HashMap; pub type SummaryRowContent = HashMap; pub type TranslateRowContent = Vec; @@ -14,8 +13,8 @@ pub type TranslateRowContent = Vec; pub trait DatabaseAIService: Send + Sync { async fn summary_database_row( &self, - _workspace_id: &Uuid, - _object_id: &Uuid, + _workspace_id: &str, + _object_id: &str, _summary_row: SummaryRowContent, ) -> Result { Ok("".to_string()) @@ -23,7 +22,7 @@ pub trait DatabaseAIService: Send + Sync { async fn translate_database_row( &self, - _workspace_id: &Uuid, + _workspace_id: &str, _translate_row: TranslateRowContent, _language: &str, ) -> Result { @@ -42,29 +41,29 @@ pub trait DatabaseAIService: Send + Sync { pub trait DatabaseCloudService: Send + Sync { async fn get_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError>; async fn create_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError>; async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result; async fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, + object_id: &str, limit: usize, ) -> Result, FlowyError>; } diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index ec0eb94210..a6b676512d 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -51,7 +51,6 @@ strum_macros = "0.25" validator = { workspace = true, features = ["derive"] } tokio-util.workspace = true moka = { version = "0.12.8", features = ["future"] } -uuid.workspace = true [dev-dependencies] event-integration-test = { path = "../event-integration-test", default-features = false } @@ -63,4 +62,5 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] verbose_log = ["collab-database/verbose_log"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs index e10aed7956..aeaaee42f3 100644 --- a/frontend/rust-lib/flowy-database2/build.rs +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -5,19 +5,19 @@ fn main() { flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - // #[cfg(feature = "ts")] - // { - // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - // flowy_codegen::protobuf_file::ts_gen( - // env!("CARGO_PKG_NAME"), - // env!("CARGO_PKG_NAME"), - // flowy_codegen::Project::Tauri, - // ); - // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - // flowy_codegen::protobuf_file::ts_gen( - // env!("CARGO_PKG_NAME"), - // env!("CARGO_PKG_NAME"), - // flowy_codegen::Project::TauriApp, - // ); - // } + #[cfg(feature = "ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 2562bd84f7..8c16db4379 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -26,6 +26,9 @@ pub struct DatabasePB { #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, + + #[pb(index = 5)] + pub is_linked: bool, } #[derive(ProtoBuf, Default)] @@ -205,7 +208,7 @@ pub struct DatabaseMetaPB { pub database_id: String, #[pb(index = 2)] - pub view_id: String, + pub inline_view_id: String, } #[derive(Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 63d6fdf2c3..ed766885c7 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -865,25 +865,17 @@ pub(crate) async fn delete_group_handler( } #[tracing::instrument(level = "debug", skip(manager), err)] -pub(crate) async fn get_default_database_view_id_handler( +pub(crate) async fn get_database_meta_handler( data: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let manager = upgrade_manager(manager)?; let database_id = data.into_inner().value; - let database_view_id = manager - .get_database_meta(&database_id) - .await? - .and_then(|mut d| d.linked_views.pop()) - .ok_or_else(|| { - FlowyError::internal().with_context(format!( - "Can't find any database view for given database id: {}", - database_id - )) - })?; + let inline_view_id = manager.get_database_inline_view_id(&database_id).await?; - let data = DatabaseViewIdPB { - value: database_view_id, + let data = DatabaseMetaPB { + database_id, + inline_view_id, }; data_result_ok(data) } @@ -900,7 +892,7 @@ pub(crate) async fn get_databases_handler( if let Some(link_view) = meta.linked_views.first() { items.push(DatabaseMetaPB { database_id: meta.database_id, - view_id: link_view.clone(), + inline_view_id: link_view.clone(), }) } } @@ -1269,7 +1261,7 @@ pub(crate) async fn summarize_row_handler( let (tx, rx) = oneshot::channel(); tokio::spawn(async move { let result = manager - .summarize_row(&data.view_id, row_id, data.field_id) + .summarize_row(data.view_id, row_id, data.field_id) .await; let _ = tx.send(result); }); @@ -1288,7 +1280,7 @@ pub(crate) async fn translate_row_handler( let (tx, rx) = oneshot::channel(); tokio::spawn(async move { let result = manager - .translate_row(&data.view_id, row_id, data.field_id) + .translate_row(data.view_id, row_id, data.field_id) .await; let _ = tx.send(result); }); diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 824565e5b8..6281cde745 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -64,7 +64,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::CreateGroup, create_group_handler) .event(DatabaseEvent::DeleteGroup, delete_group_handler) // Database - .event(DatabaseEvent::GetDefaultDatabaseViewId, get_default_database_view_id_handler) + .event(DatabaseEvent::GetDatabaseMeta, get_database_meta_handler) .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) @@ -305,8 +305,8 @@ pub enum DatabaseEvent { #[event(input = "DeleteGroupPayloadPB")] DeleteGroup = 115, - #[event(input = "DatabaseIdPB", output = "DatabaseViewIdPB")] - GetDefaultDatabaseViewId = 119, + #[event(input = "DatabaseIdPB", output = "DatabaseMetaPB")] + GetDatabaseMeta = 119, /// Returns all the databases #[event(output = "RepeatedDatabaseDescriptionPB")] diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 666d2f8eaf..fca8db4f97 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -20,7 +20,6 @@ use collab_entity::{CollabObject, CollabType, EncodedCollab}; use collab_plugins::local_storage::kv::KVTransactionDB; use rayon::prelude::*; use std::collections::HashMap; -use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; use tokio::sync::Mutex; @@ -43,13 +42,12 @@ use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::field_settings::default_field_settings_by_layout_map; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; use tokio::sync::RwLock as TokioRwLock; -use uuid::Uuid; pub trait DatabaseUser: Send + Sync { fn user_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; - fn workspace_id(&self) -> Result; - fn workspace_database_object_id(&self) -> Result; + fn workspace_id(&self) -> Result; + fn workspace_database_object_id(&self) -> Result; } pub(crate) type DatabaseEditorMap = HashMap>; @@ -112,7 +110,7 @@ impl DatabaseManager { let workspace_database_object_id = self.user.workspace_database_object_id()?; let workspace_database_collab = collab_service .build_collab( - workspace_database_object_id.to_string().as_str(), + workspace_database_object_id.as_str(), CollabType::WorkspaceDatabase, None, ) @@ -134,12 +132,12 @@ impl DatabaseManager { } #[instrument( - name = "database_initialize_after_sign_up", + name = "database_initialize_with_new_user", level = "debug", skip_all, err )] - pub async fn initialize_after_sign_up( + pub async fn initialize_with_new_user( &self, user_id: i64, is_local_user: bool, @@ -148,22 +146,13 @@ impl DatabaseManager { Ok(()) } - pub async fn initialize_after_open_workspace( - &self, - user_id: i64, - is_local_user: bool, - ) -> FlowyResult<()> { - self.initialize(user_id, is_local_user).await?; - Ok(()) - } - - pub async fn initialize_after_sign_in( - &self, - user_id: i64, - is_local_user: bool, - ) -> FlowyResult<()> { - self.initialize(user_id, is_local_user).await?; - Ok(()) + pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { + let lock = self.workspace_database()?; + let wdb = lock.read().await; + let database_collab = wdb.get_or_init_database(database_id).await?; + drop(wdb); + let lock_guard = database_collab.read().await; + Ok(lock_guard.get_inline_view_id()) } pub async fn get_all_databases_meta(&self) -> Vec { @@ -175,15 +164,6 @@ impl DatabaseManager { items } - pub async fn get_database_meta(&self, database_id: &str) -> FlowyResult> { - let mut database_meta = None; - if let Some(lock) = self.workspace_database_manager.load_full() { - let wdb = lock.read().await; - database_meta = wdb.get_database_meta(database_id); - } - Ok(database_meta) - } - #[instrument(level = "trace", skip_all, err)] pub async fn update_database_indexing( &self, @@ -209,10 +189,8 @@ impl DatabaseManager { }) } - pub async fn encode_database(&self, view_id: &Uuid) -> FlowyResult { - let editor = self - .get_database_editor_with_view_id(view_id.to_string().as_str()) - .await?; + pub async fn encode_database(&self, view_id: &str) -> FlowyResult { + let editor = self.get_database_editor_with_view_id(view_id).await?; let collabs = editor .database .read() @@ -229,12 +207,10 @@ impl DatabaseManager { pub async fn get_database_row_metas_with_view_id( &self, - view_id: &Uuid, + view_id: &str, row_ids: Vec, ) -> FlowyResult> { - let database = self - .get_database_editor_with_view_id(view_id.to_string().as_str()) - .await?; + let database = self.get_database_editor_with_view_id(view_id).await?; let view_id = view_id.to_string(); let mut row_metas: Vec = vec![]; for row_id in row_ids { @@ -299,11 +275,11 @@ impl DatabaseManager { /// Open the database view #[instrument(level = "trace", skip_all, err)] - pub async fn open_database_view(&self, view_id: &Uuid) -> FlowyResult<()> { - let view_id = view_id.to_string(); + pub async fn open_database_view>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); let lock = self.workspace_database()?; let workspace_database = lock.read().await; - let result = workspace_database.get_database_id_with_view_id(&view_id); + let result = workspace_database.get_database_id_with_view_id(view_id); drop(workspace_database); if let Some(database_id) = result { @@ -316,7 +292,8 @@ impl DatabaseManager { } #[instrument(level = "trace", skip_all, err)] - pub async fn close_database_view(&self, view_id: &str) -> FlowyResult<()> { + pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); let lock = self.workspace_database()?; let workspace_database = lock.read().await; let database_id = workspace_database.get_database_id_with_view_id(view_id); @@ -541,9 +518,7 @@ impl DatabaseManager { layout: DatabaseLayoutPB, ) -> FlowyResult<()> { let database = self.get_database_editor_with_view_id(view_id).await?; - database - .update_view_layout(view_id.to_string().as_str(), layout.into()) - .await + database.update_view_layout(view_id, layout.into()).await } pub async fn get_database_snapshots( @@ -551,7 +526,7 @@ impl DatabaseManager { view_id: &str, limit: usize, ) -> FlowyResult> { - let database_id = Uuid::from_str(&self.get_database_id_with_view_id(view_id).await?)?; + let database_id = self.get_database_id_with_view_id(view_id).await?; let snapshots = self .cloud_service .get_database_collab_object_snapshots(&database_id, limit) @@ -578,14 +553,14 @@ impl DatabaseManager { #[instrument(level = "debug", skip_all)] pub async fn summarize_row( &self, - view_id: &str, + view_id: String, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; + let database = self.get_database_editor_with_view_id(&view_id).await?; let mut summary_row_content = SummaryRowContent::new(); - if let Some(row) = database.get_row(view_id, &row_id).await { - let fields = database.get_fields(view_id, None).await; + if let Some(row) = database.get_row(&view_id, &row_id).await { + let fields = database.get_fields(&view_id, None).await; for field in fields { // When summarizing a row, skip the content in the "AI summary" cell; it does not need to // be summarized. @@ -608,17 +583,13 @@ impl DatabaseManager { ); let response = self .ai_service - .summary_database_row( - &self.user.workspace_id()?, - &Uuid::from_str(&row_id)?, - summary_row_content, - ) + .summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content) .await?; trace!("[AI]:summarize row response: {}", response); // Update the cell with the response from the cloud service. database - .update_cell_with_changeset(view_id, &row_id, &field_id, BoxAny::new(response)) + .update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response)) .await?; Ok(()) } @@ -626,12 +597,11 @@ impl DatabaseManager { #[instrument(level = "debug", skip_all)] pub async fn translate_row( &self, - view_id: &str, + view_id: String, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; - let view_id = view_id.to_string(); + let database = self.get_database_editor_with_view_id(&view_id).await?; let mut translate_row_content = TranslateRowContent::new(); let mut language = "english".to_string(); @@ -733,13 +703,10 @@ impl WorkspaceDatabaseCollabServiceImpl { async fn get_encode_collab( &self, - object_id: &Uuid, + object_id: &str, object_ty: CollabType, ) -> Result, DatabaseError> { - let workspace_id = self - .user - .workspace_id() - .map_err(|e| DatabaseError::Internal(e.into()))?; + let workspace_id = self.user.workspace_id().unwrap(); trace!("[Database]: fetch {}:{} from remote", object_id, object_ty); let encode_collab = self .cloud_service @@ -751,7 +718,7 @@ impl WorkspaceDatabaseCollabServiceImpl { async fn batch_get_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, ) -> Result { let workspace_id = self @@ -763,13 +730,7 @@ impl WorkspaceDatabaseCollabServiceImpl { .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) .await .map_err(|err| DatabaseError::Internal(err.into()))?; - - Ok( - updates - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect(), - ) + Ok(updates) } fn collab_db(&self) -> Result, DatabaseError> { @@ -785,7 +746,7 @@ impl WorkspaceDatabaseCollabServiceImpl { fn build_collab_object( &self, - object_id: &Uuid, + object_id: &str, object_type: CollabType, ) -> Result { let uid = self @@ -815,12 +776,8 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { collab_type: CollabType, encoded_collab: Option<(EncodedCollab, bool)>, ) -> Result { - let object_id = Uuid::parse_str(object_id)?; - let object = self.build_collab_object(&object_id, collab_type)?; - let data_source = if self - .persistence - .is_collab_exist(object_id.to_string().as_str()) - { + let object = self.build_collab_object(object_id, collab_type.clone())?; + let data_source = if self.persistence.is_collab_exist(object_id) { trace!( "build collab: {}:{} from local encode collab", collab_type, @@ -839,7 +796,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { object_id, encoded_collab.is_none(), ); - match self.get_encode_collab(&object_id, collab_type).await { + match self.get_encode_collab(object_id, collab_type.clone()).await { Ok(Some(encode_collab)) => { info!( "build collab: {}:{} with remote encode collab, {} bytes", @@ -880,11 +837,12 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { ); self .persistence - .save_collab(object_id.to_string().as_str(), encoded_collab.clone())?; + .save_collab(object_id, encoded_collab.clone())?; // TODO(nathan): cover database rows and other database collab type if matches!(collab_type, CollabType::Database) { if let Ok(workspace_id) = self.user.workspace_id() { + let object_id = object_id.to_string(); let cloned_encoded_collab = encoded_collab.clone(); let cloud_service = self.cloud_service.clone(); tokio::spawn(async move { @@ -920,7 +878,6 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { if object_ids.is_empty() { return Ok(EncodeCollabByOid::new()); } - let mut encoded_collab_by_id = EncodeCollabByOid::new(); // 1. Collect local disk collabs into a HashMap let local_disk_encoded_collab: HashMap = object_ids @@ -928,7 +885,7 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { .filter_map(|object_id| { self .persistence - .get_encoded_collab(object_id.as_str(), collab_type) + .get_encoded_collab(object_id.as_str(), collab_type.clone()) .map(|encoded_collab| (object_id.clone(), encoded_collab)) }) .collect(); @@ -943,10 +900,6 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { } if !object_ids.is_empty() { - let object_ids = object_ids - .into_iter() - .flat_map(|v| Uuid::from_str(&v).ok()) - .collect::>(); // 2. Fetch remaining collabs from remote let remote_collabs = self .batch_get_encode_collab(object_ids, collab_type) @@ -974,7 +927,7 @@ pub struct DatabasePersistenceImpl { } impl DatabasePersistenceImpl { - fn workspace_id(&self) -> Result { + fn workspace_id(&self) -> Result { let workspace_id = self .user .workspace_id() @@ -994,7 +947,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { if let Ok((uid, Ok(Some(collab_db)))) = result { let object_id = collab.object_id().to_string(); let db_read = collab_db.read_txn(); - if !db_read.is_exist(uid, workspace_id.to_string().as_str(), &object_id) { + if !db_read.is_exist(uid, &workspace_id, &object_id) { trace!( "[Database]: collab:{} not exist in local storage", object_id @@ -1004,12 +957,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { trace!("[Database]: start loading collab:{} from disk", object_id); let mut txn = collab.transact_mut(); - match db_read.load_doc_with_txn( - uid, - workspace_id.to_string().as_str(), - &object_id, - &mut txn, - ) { + match db_read.load_doc_with_txn(uid, &workspace_id, &object_id, &mut txn) { Ok(update_count) => { trace!( "[Database]: did load collab:{}, update_count:{}", @@ -1028,7 +976,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { } fn get_encoded_collab(&self, object_id: &str, collab_type: CollabType) -> Option { - let workspace_id = self.user.workspace_id().ok()?.to_string(); + let workspace_id = self.user.workspace_id().ok()?; let uid = self.user.user_id().ok()?; let db = self.user.collab_db(uid).ok()?.upgrade()?; let read_txn = db.read_txn(); @@ -1047,7 +995,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { } fn delete_collab(&self, object_id: &str) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?.to_string(); + let workspace_id = self.workspace_id()?; let uid = self .user .user_id() @@ -1069,7 +1017,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { object_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?.to_string(); + let workspace_id = self.workspace_id()?; let uid = self .user .user_id() @@ -1103,7 +1051,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { Ok(uid) => { if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { let read_txn = collab_db.read_txn(); - return read_txn.is_exist(uid, workspace_id.to_string().as_str(), object_id); + return read_txn.is_exist(uid, workspace_id.as_str(), object_id); } false }, @@ -1125,8 +1073,7 @@ impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { let workspace_id = self .user .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))? - .to_string(); + .map_err(|err| DatabaseError::Internal(err.into()))?; if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { let write_txn = collab_db.write_txn(); diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 227b96df4f..e284466054 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -44,7 +44,6 @@ use lib_infra::box_any::BoxAny; use lib_infra::priority_task::TaskDispatcher; use lib_infra::util::timestamp; use std::collections::HashMap; -use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; use tokio::select; @@ -54,12 +53,11 @@ use tokio::sync::{broadcast, oneshot}; use tokio::task::yield_now; use tokio_util::sync::CancellationToken; use tracing::{debug, error, event, info, instrument, trace, warn}; -use uuid::Uuid; type OpenDatabaseResult = oneshot::Sender>; pub struct DatabaseEditor { - database_id: Uuid, + database_id: String, pub(crate) database: Arc>, pub cell_cache: CellCache, pub(crate) database_views: Arc, @@ -119,7 +117,6 @@ impl DatabaseEditor { .await?, ); - let database_id = Uuid::from_str(&database_id)?; let collab_object = collab_builder.collab_object( &user.workspace_id()?, user.user_id()?, @@ -133,7 +130,7 @@ impl DatabaseEditor { database.clone(), )?; let this = Arc::new(Self { - database_id, + database_id: database_id.clone(), user, database, cell_cache, @@ -809,11 +806,10 @@ impl DatabaseEditor { let is_finalized = self.finalized_rows.get(row_id.as_str()).await.is_some(); if !is_finalized { trace!("[Database]: finalize database row: {}", row_id); - let row_id = Uuid::from_str(row_id.as_str())?; let collab_object = self.collab_builder.collab_object( &self.user.workspace_id()?, self.user.user_id()?, - &row_id, + row_id, CollabType::DatabaseRow, )?; @@ -1505,7 +1501,7 @@ impl DatabaseEditor { view_editor.set_row_orders(row_orders.clone()).await; // Collect database details in a single block holding the `read` lock - let (database_id, fields) = { + let (database_id, fields, is_linked) = { let database = self.database.read().await; ( database.get_database_id(), @@ -1514,6 +1510,7 @@ impl DatabaseEditor { .into_iter() .map(FieldIdPB::from) .collect::>(), + database.is_inline_view(view_id), ) }; @@ -1556,6 +1553,7 @@ impl DatabaseEditor { fields, rows: order_rows, layout_type: view_layout.into(), + is_linked, }); // Mark that the opening process is complete if let Some(tx) = self.is_loading_rows.load_full() { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index 1c965995ec..081d23f1b3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -16,7 +16,6 @@ use futures::StreamExt; use std::sync::Arc; use tracing::{error, trace, warn}; -use uuid::Uuid; pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc>) { let weak_database = Arc::downgrade(database); @@ -113,7 +112,7 @@ pub(crate) async fn observe_field_change(database_id: &str, database: &Arc) { +pub(crate) async fn observe_view_change(database_id: &str, database_editor: &Arc) { let database_id = database_id.to_string(); let weak_database_editor = Arc::downgrade(database_editor); let view_change = database_editor @@ -290,7 +289,7 @@ async fn handle_did_update_row_orders( } } -pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Arc) { +pub(crate) async fn observe_block_event(database_id: &str, database_editor: &Arc) { let database_id = database_id.to_string(); let mut block_event_rx = database_editor .database diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index 3eab243fd7..dd704f43d5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -28,10 +28,8 @@ impl CSVExport { style: CSVFormat, ) -> FlowyResult { let mut wtr = csv::Writer::from_writer(vec![]); - let view_id = database - .get_first_database_view_id() - .ok_or_else(|| FlowyError::internal().with_context("failed to get first database view"))?; - let fields = database.get_fields_in_view(&view_id, None); + let inline_view_id = database.get_inline_view_id(); + let fields = database.get_fields_in_view(&inline_view_id, None); // Write fields let field_records = fields @@ -51,7 +49,7 @@ impl CSVExport { field_by_field_id.insert(field.id.clone(), field); }); let rows = database - .get_rows_for_view(&view_id, 20, None) + .get_rows_for_view(&inline_view_id, 20, None) .await .filter_map(|result| async { result.ok() }) .collect::>() diff --git a/frontend/rust-lib/flowy-date/Cargo.toml b/frontend/rust-lib/flowy-date/Cargo.toml index d04dfd8416..40015cad77 100644 --- a/frontend/rust-lib/flowy-date/Cargo.toml +++ b/frontend/rust-lib/flowy-date/Cargo.toml @@ -24,3 +24,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-date/build.rs b/frontend/rust-lib/flowy-date/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-date/build.rs +++ b/frontend/rust-lib/flowy-date/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-document-pub/Cargo.toml b/frontend/rust-lib/flowy-document-pub/Cargo.toml index cbb74de5c4..93a282f5cc 100644 --- a/frontend/rust-lib/flowy-document-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-document-pub/Cargo.toml @@ -9,5 +9,5 @@ edition = "2021" lib-infra = { workspace = true } flowy-error = { workspace = true } collab-document = { workspace = true } -collab = { workspace = true } -uuid.workspace = true \ No newline at end of file +anyhow.workspace = true +collab = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document-pub/src/cloud.rs b/frontend/rust-lib/flowy-document-pub/src/cloud.rs index d5c25053a8..f34a91bfd4 100644 --- a/frontend/rust-lib/flowy-document-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-pub/src/cloud.rs @@ -1,8 +1,8 @@ use collab::entity::EncodedCollab; pub use collab_document::blocks::DocumentData; + use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use uuid::Uuid; /// A trait for document cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of @@ -11,27 +11,27 @@ use uuid::Uuid; pub trait DocumentCloudService: Send + Sync + 'static { async fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError>; async fn get_document_snapshots( &self, - document_id: &Uuid, + document_id: &str, limit: usize, workspace_id: &str, ) -> Result, FlowyError>; async fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError>; async fn create_document_collab( &self, - workspace_id: &Uuid, - document_id: &Uuid, + workspace_id: &str, + document_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError>; } diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index aaaef4938e..77aa321d3c 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -50,5 +50,10 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = [ + "flowy-codegen/ts", +] + # search "Enable/Disable AppFlowy Verbose Log" to find the place that can enable verbose log verbose_log = ["collab-document/verbose_log"] diff --git a/frontend/rust-lib/flowy-document/build.rs b/frontend/rust-lib/flowy-document/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-document/build.rs +++ b/frontend/rust-lib/flowy-document/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index aa871cf4bc..ffdd0c900e 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -6,10 +6,9 @@ use collab::preclude::Collab; use collab_document::document::Document; use futures::StreamExt; use lib_infra::sync_trace; -use uuid::Uuid; -pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) { - let doc_id_clone_for_block_changed = doc_id.to_string(); +pub fn subscribe_document_changed(doc_id: &str, document: &mut Document) { + let doc_id_clone_for_block_changed = doc_id.to_owned(); document.subscribe_block_changed("key", move |events, is_remote| { sync_trace!( "[Document] block changed in doc_id: {}, is_remote: {}, events: {:?}", @@ -36,7 +35,7 @@ pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) { ); document_notification_builder( - &doc_id_clone_for_awareness_state.to_string(), + &doc_id_clone_for_awareness_state, DocumentNotification::DidUpdateDocumentAwarenessState, ) .payload::(events.into()) diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index c8a6765fd6..74157c6124 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use collab::core::collab_state::SyncState; use collab_document::{ blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData}, @@ -6,12 +8,10 @@ use collab_document::{ DocumentAwarenessUser, }, }; + use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use lib_infra::validator_fn::{required_not_empty_str, required_valid_path}; -use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; use validator::Validate; use crate::parse::{NotEmptyStr, NotEmptyVec}; @@ -31,7 +31,7 @@ pub struct OpenDocumentPayloadPB { } pub struct OpenDocumentParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto for OpenDocumentPayloadPB { @@ -39,9 +39,9 @@ impl TryInto for OpenDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - - Ok(OpenDocumentParams { document_id }) + Ok(OpenDocumentParams { + document_id: document_id.0, + }) } } @@ -52,7 +52,7 @@ pub struct DocumentRedoUndoPayloadPB { } pub struct DocumentRedoUndoParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto for DocumentRedoUndoPayloadPB { @@ -60,8 +60,9 @@ impl TryInto for DocumentRedoUndoPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - Ok(DocumentRedoUndoParams { document_id }) + Ok(DocumentRedoUndoParams { + document_id: document_id.0, + }) } } @@ -131,7 +132,7 @@ pub struct CreateDocumentPayloadPB { } pub struct CreateDocumentParams { - pub document_id: Uuid, + pub document_id: String, pub initial_data: Option, } @@ -140,10 +141,9 @@ impl TryInto for CreateDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let initial_data = self.initial_data.map(|data| data.into()); Ok(CreateDocumentParams { - document_id, + document_id: document_id.0, initial_data, }) } @@ -156,7 +156,7 @@ pub struct CloseDocumentPayloadPB { } pub struct CloseDocumentParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto for CloseDocumentPayloadPB { @@ -164,8 +164,9 @@ impl TryInto for CloseDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - Ok(CloseDocumentParams { document_id }) + Ok(CloseDocumentParams { + document_id: document_id.0, + }) } } @@ -179,7 +180,7 @@ pub struct ApplyActionPayloadPB { } pub struct ApplyActionParams { - pub document_id: Uuid, + pub document_id: String, pub actions: Vec, } @@ -188,11 +189,10 @@ impl TryInto for ApplyActionPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?; let actions = actions.0.into_iter().map(BlockAction::from).collect(); Ok(ApplyActionParams { - document_id, + document_id: document_id.0, actions, }) } @@ -525,7 +525,7 @@ pub struct TextDeltaPayloadPB { } pub struct TextDeltaParams { - pub document_id: Uuid, + pub document_id: String, pub text_id: String, pub delta: String, } @@ -535,11 +535,10 @@ impl TryInto for TextDeltaPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let text_id = NotEmptyStr::parse(self.text_id).map_err(|_| ErrorCode::TextIdIsEmpty)?; let delta = self.delta.map_or_else(|| "".to_string(), |delta| delta); Ok(TextDeltaParams { - document_id, + document_id: document_id.0, text_id: text_id.0, delta, }) diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index acf45777eb..387e216f08 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -3,7 +3,7 @@ * as well as performing actions on documents. These functions make use of a DocumentManager, * which you can think of as a higher-level interface to interact with documents. */ -use std::str::FromStr; + use std::sync::{Arc, Weak}; use collab_document::blocks::{ @@ -11,6 +11,10 @@ use collab_document::blocks::{ DocumentData, }; +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use tracing::instrument; + use crate::entities::*; use crate::parser::document_data_parser::DocumentDataParser; use crate::parser::external::parser::ExternalDataToNestedJSONParser; @@ -19,11 +23,6 @@ use crate::parser::parser_entities::{ ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, }; use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use lib_infra::sync_trace; -use tracing::instrument; -use uuid::Uuid; fn upgrade_document( document_manager: AFPluginState>, @@ -125,7 +124,9 @@ pub(crate) async fn apply_action_handler( let doc_id = params.document_id; let document = manager.editable_document(&doc_id).await?; let actions = params.actions; - sync_trace!("{} applying action: {:?}", doc_id, actions); + if cfg!(feature = "verbose_log") { + tracing::trace!("{} applying actions: {:?}", doc_id, actions); + } document.write().await.apply_action(actions)?; Ok(()) } @@ -140,7 +141,6 @@ pub(crate) async fn create_text_handler( let doc_id = params.document_id; let document = manager.editable_document(&doc_id).await?; let mut document = document.write().await; - sync_trace!("{} creating text: {:?}", doc_id, params.delta); document.apply_text_delta(¶ms.text_id, params.delta); Ok(()) } @@ -157,7 +157,9 @@ pub(crate) async fn apply_text_delta_handler( let text_id = params.text_id; let delta = params.delta; let mut document = document.write().await; - sync_trace!("{} applying delta: {:?}", doc_id, delta); + if cfg!(feature = "verbose_log") { + tracing::trace!("{} applying delta: {:?}", doc_id, delta); + } document.apply_text_delta(&text_id, delta); Ok(()) } @@ -497,7 +499,7 @@ pub(crate) async fn set_awareness_local_state_handler( ) -> FlowyResult<()> { let manager = upgrade_document(manager)?; let data = data.into_inner(); - let doc_id = Uuid::from_str(&data.document_id)?; + let doc_id = data.document_id.clone(); manager .set_document_awareness_local_state(&doc_id, data) .await?; diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9c6a383bae..b84469872b 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -14,21 +14,21 @@ use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; +use collab_plugins::CollabKVDB; +use dashmap::DashMap; +use lib_infra::util::timestamp; +use tracing::{event, instrument}; +use tracing::{info, trace}; + use crate::document::{ subscribe_document_changed, subscribe_document_snapshot_state, subscribe_document_sync_state, }; use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, }; -use collab_plugins::CollabKVDB; -use dashmap::DashMap; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::storage::{CreatedUpload, StorageService}; -use lib_infra::util::timestamp; -use tracing::{event, instrument}; -use tracing::{info, trace}; -use uuid::Uuid; use crate::entities::UpdateDocumentAwarenessStatePB; use crate::entities::{ @@ -39,7 +39,7 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUserService: Send + Sync { fn user_id(&self) -> Result; fn device_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; } @@ -54,8 +54,8 @@ pub trait DocumentSnapshotService: Send + Sync { pub struct DocumentManager { pub user_service: Arc, collab_builder: Arc, - documents: Arc>>>, - removing_documents: Arc>>>, + documents: Arc>>>, + removing_documents: Arc>>>, cloud_service: Arc, storage_service: Weak, snapshot_service: Arc, @@ -81,7 +81,7 @@ impl DocumentManager { } /// Get the encoded collab of the document. - pub async fn get_encoded_collab_with_view_id(&self, doc_id: &Uuid) -> FlowyResult { + pub async fn get_encoded_collab_with_view_id(&self, doc_id: &str) -> FlowyResult { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; let doc_state = @@ -106,23 +106,12 @@ impl DocumentManager { } #[instrument( - name = "document_initialize_after_sign_up", + name = "document_initialize_with_new_user", level = "debug", skip_all, err )] - pub async fn initialize_after_sign_up(&self, uid: i64) -> FlowyResult<()> { - self.initialize(uid).await?; - Ok(()) - } - - pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { - self.initialize(uid).await?; - Ok(()) - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn initialize_after_sign_in(&self, uid: i64) -> FlowyResult<()> { + pub async fn initialize_with_new_user(&self, uid: i64) -> FlowyResult<()> { self.initialize(uid).await?; Ok(()) } @@ -150,7 +139,7 @@ impl DocumentManager { pub async fn create_document( &self, _uid: i64, - doc_id: &Uuid, + doc_id: &str, data: Option, ) -> FlowyResult { if self.is_doc_exist(doc_id).await.unwrap_or(false) { @@ -162,17 +151,17 @@ impl DocumentManager { let encoded_collab = doc_state_from_document_data(doc_id, data).await?; self .persistence()? - .save_collab_to_disk(doc_id.to_string().as_str(), encoded_collab.clone()) + .save_collab_to_disk(doc_id, encoded_collab.clone()) .map_err(internal_error)?; // Send the collab data to server with a background task. let cloud_service = self.cloud_service.clone(); let cloned_encoded_collab = encoded_collab.clone(); + let document_id = doc_id.to_string(); let workspace_id = self.user_service.workspace_id()?; - let doc_id = *doc_id; tokio::spawn(async move { let _ = cloud_service - .create_document_collab(&workspace_id, &doc_id, cloned_encoded_collab) + .create_document_collab(&workspace_id, &document_id, cloned_encoded_collab) .await; }); Ok(encoded_collab) @@ -182,7 +171,7 @@ impl DocumentManager { async fn collab_for_document( &self, uid: i64, - doc_id: &Uuid, + doc_id: &str, data_source: DataSource, sync_enable: bool, ) -> FlowyResult>> { @@ -206,7 +195,7 @@ impl DocumentManager { } /// Return a document instance if the document is already opened. - pub async fn editable_document(&self, doc_id: &Uuid) -> FlowyResult>> { + pub async fn editable_document(&self, doc_id: &str) -> FlowyResult>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -224,7 +213,7 @@ impl DocumentManager { #[tracing::instrument(level = "info", skip(self), err)] async fn create_document_instance( &self, - doc_id: &Uuid, + doc_id: &str, enable_sync: bool, ) -> FlowyResult>> { let uid = self.user_service.user_id()?; @@ -271,7 +260,7 @@ impl DocumentManager { subscribe_document_snapshot_state(&lock); subscribe_document_sync_state(&lock); } - self.documents.insert(*doc_id, document.clone()); + self.documents.insert(doc_id.to_string(), document.clone()); } Ok(document) }, @@ -284,21 +273,21 @@ impl DocumentManager { } } - pub async fn get_document_data(&self, doc_id: &Uuid) -> FlowyResult { + pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { let document = self.get_document(doc_id).await?; let document = document.read().await; document.get_document_data().map_err(internal_error) } - pub async fn get_document_text(&self, doc_id: &Uuid) -> FlowyResult { + pub async fn get_document_text(&self, doc_id: &str) -> FlowyResult { let document = self.get_document(doc_id).await?; let document = document.read().await; - let text = document.paragraphs().join("\n"); + let text = document.to_plain_text(true, false)?; Ok(text) } /// Return a document instance. /// The returned document might or might not be able to sync with the cloud. - async fn get_document(&self, doc_id: &Uuid) -> FlowyResult>> { + async fn get_document(&self, doc_id: &str) -> FlowyResult>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -311,7 +300,7 @@ impl DocumentManager { Ok(document) } - pub async fn open_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some(mutex_document) = self.restore_document_from_removing(doc_id) { let lock = mutex_document.read().await; lock.start_init_sync(); @@ -325,7 +314,7 @@ impl DocumentManager { Ok(()) } - pub async fn close_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some((doc_id, document)) = self.documents.remove(doc_id) { { // clear the awareness state when close the document @@ -333,7 +322,7 @@ impl DocumentManager { lock.clean_awareness_local_state(); } - let clone_doc_id = doc_id; + let clone_doc_id = doc_id.clone(); trace!("move document to removing_documents: {}", doc_id); self.removing_documents.insert(doc_id, document); @@ -351,12 +340,11 @@ impl DocumentManager { Ok(()) } - pub async fn delete_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn delete_document(&self, doc_id: &str) -> FlowyResult<()> { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; if let Some(db) = self.user_service.collab_db(uid)?.upgrade() { - db.delete_doc(uid, &workspace_id.to_string(), &doc_id.to_string()) - .await?; + db.delete_doc(uid, &workspace_id, doc_id).await?; // When deleting a document, we need to remove it from the cache. self.documents.remove(doc_id); } @@ -366,7 +354,7 @@ impl DocumentManager { #[instrument(level = "debug", skip_all, err)] pub async fn set_document_awareness_local_state( &self, - doc_id: &Uuid, + doc_id: &str, state: UpdateDocumentAwarenessStatePB, ) -> FlowyResult { let uid = self.user_service.user_id()?; @@ -391,12 +379,12 @@ impl DocumentManager { /// Return the list of snapshots of the document. pub async fn get_document_snapshot_meta( &self, - document_id: &Uuid, + document_id: &str, _limit: usize, ) -> FlowyResult> { let metas = self .snapshot_service - .get_document_snapshot_metas(document_id.to_string().as_str())? + .get_document_snapshot_metas(document_id)? .into_iter() .map(|meta| DocumentSnapshotMetaPB { snapshot_id: meta.snapshot_id, @@ -446,13 +434,11 @@ impl DocumentManager { Ok(()) } - async fn is_doc_exist(&self, doc_id: &Uuid) -> FlowyResult { + async fn is_doc_exist(&self, doc_id: &str) -> FlowyResult { let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() { - let is_exist = collab_db - .is_exist(uid, &workspace_id.to_string(), &doc_id.to_string()) - .await?; + let is_exist = collab_db.is_exist(uid, &workspace_id, doc_id).await?; Ok(is_exist) } else { Ok(false) @@ -477,7 +463,7 @@ impl DocumentManager { &self.storage_service } - fn restore_document_from_removing(&self, doc_id: &Uuid) -> Option>> { + fn restore_document_from_removing(&self, doc_id: &str) -> Option>> { let (doc_id, doc) = self.removing_documents.remove(doc_id)?; trace!( "move document {} from removing_documents to documents", @@ -489,17 +475,11 @@ impl DocumentManager { } async fn doc_state_from_document_data( - doc_id: &Uuid, + doc_id: &str, data: Option, ) -> Result { let doc_id = doc_id.to_string(); - let data = data.unwrap_or_else(|| { - trace!( - "{} document data is None, use default document data", - doc_id.to_string() - ); - default_document_data(&doc_id) - }); + let data = data.unwrap_or_else(|| default_document_data(&doc_id)); // spawn_blocking is used to avoid blocking the tokio thread pool if the document is large. let encoded_collab = tokio::task::spawn_blocking(move || { let collab = Collab::new_with_origin(CollabOrigin::Empty, doc_id, vec![], false); diff --git a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs index 94680b32d3..8acdecae36 100644 --- a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs +++ b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; -use uuid::Uuid; use validator::Validate; #[derive(Default, ProtoBuf)] @@ -97,7 +96,7 @@ pub struct ParseType { } pub struct ConvertDocumentParams { - pub document_id: Uuid, + pub document_id: String, pub range: Option, pub parse_types: ParseType, } @@ -141,11 +140,10 @@ impl TryInto for ConvertDocumentPayloadPB { fn try_into(self) -> Result { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::parse_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let range = self.range.map(|data| data.into()); Ok(ConvertDocumentParams { - document_id, + document_id: document_id.0, range, parse_types: self.parse_types.into(), }) diff --git a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs index 2a47ec93c4..b11cd2ecde 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs @@ -9,8 +9,8 @@ use crate::document::util::{gen_document_id, gen_id, DocumentTest}; async fn undo_redo_test() { let test = DocumentTest::new(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test diff --git a/frontend/rust-lib/flowy-document/tests/document/document_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_test.rs index 8323a645c7..d7906bc114 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_test.rs @@ -11,8 +11,8 @@ async fn restore_document() { let test = DocumentTest::new(); // create a document - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); let uid = test.user_service.user_id().unwrap(); test .create_document(uid, &doc_id, Some(data.clone())) @@ -55,8 +55,8 @@ async fn restore_document() { async fn document_apply_insert_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; @@ -111,9 +111,9 @@ async fn document_apply_insert_action() { #[tokio::test] async fn document_apply_update_page_action() { let test = DocumentTest::new(); - let doc_id = gen_document_id(); + let doc_id: String = gen_document_id(); let uid = test.user_service.user_id().unwrap(); - let data = default_document_data(&doc_id.to_string()); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; @@ -158,8 +158,8 @@ async fn document_apply_update_page_action() { async fn document_apply_update_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 231bb3852e..20e6b5d79d 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -1,11 +1,17 @@ use std::ops::Deref; use std::sync::{Arc, OnceLock}; +use anyhow::Error; use collab::entity::EncodedCollab; use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; use collab_document::document::Document; use collab_document::document_data::default_document_data; +use nanoid::nanoid; +use tempfile::TempDir; +use tokio::sync::RwLock; +use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; + use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, WorkspaceCollabIntegrate, @@ -18,11 +24,6 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_storage_pub::storage::{CreatedUpload, FileProgressReceiver, StorageService}; use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; -use nanoid::nanoid; -use tempfile::TempDir; -use tokio::sync::RwLock; -use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; -use uuid::Uuid; pub struct DocumentTest { inner: DocumentManager, @@ -38,7 +39,7 @@ impl DocumentTest { let builder = Arc::new(AppFlowyCollabBuilder::new( DefaultCollabStorageProvider(), WorkspaceCollabIntegrateImpl { - workspace_id: user.workspace_id, + workspace_id: user.workspace_id.clone(), }, )); @@ -62,7 +63,7 @@ impl Deref for DocumentTest { } pub struct FakeUser { - workspace_id: Uuid, + workspace_id: String, collab_db: Arc, } @@ -73,7 +74,7 @@ impl FakeUser { let tempdir = TempDir::new().unwrap(); let path = tempdir.into_path(); let collab_db = Arc::new(CollabKVDB::open(path).unwrap()); - let workspace_id = uuid::Uuid::new_v4(); + let workspace_id = uuid::Uuid::new_v4().to_string(); Self { collab_db, @@ -87,8 +88,8 @@ impl DocumentUserService for FakeUser { Ok(1) } - fn workspace_id(&self) -> Result { - Ok(self.workspace_id) + fn workspace_id(&self) -> Result { + Ok(self.workspace_id.clone()) } fn collab_db(&self, _uid: i64) -> Result, FlowyError> { @@ -114,8 +115,8 @@ pub fn setup_log() { pub async fn create_and_open_empty_document() -> (DocumentTest, Arc>, String) { let test = DocumentTest::new(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); let uid = test.user_service.user_id().unwrap(); // create a document test @@ -129,8 +130,9 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc Uuid { - uuid::Uuid::new_v4() +pub fn gen_document_id() -> String { + let uuid = uuid::Uuid::new_v4(); + uuid.to_string() } pub fn gen_id() -> String { @@ -143,8 +145,8 @@ pub struct LocalTestDocumentCloudServiceImpl(); impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_doc_state( &self, - document_id: &Uuid, - _workspace_id: &Uuid, + document_id: &str, + _workspace_id: &str, ) -> Result, FlowyError> { let document_id = document_id.to_string(); Err(FlowyError::new( @@ -155,7 +157,7 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_snapshots( &self, - _document_id: &Uuid, + _document_id: &str, _limit: usize, _workspace_id: &str, ) -> Result, FlowyError> { @@ -164,16 +166,16 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { async fn get_document_data( &self, - _document_id: &Uuid, - _workspace_id: &Uuid, + _document_id: &str, + _workspace_id: &str, ) -> Result, FlowyError> { Ok(None) } async fn create_document_collab( &self, - _workspace_id: &Uuid, - _document_id: &Uuid, + _workspace_id: &str, + _document_id: &str, _encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) @@ -255,14 +257,14 @@ impl DocumentSnapshotService for DocumentTestSnapshot { } struct WorkspaceCollabIntegrateImpl { - workspace_id: Uuid, + workspace_id: String, } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result { - Ok(self.workspace_id) + fn workspace_id(&self) -> Result { + Ok(self.workspace_id.clone()) } - fn device_id(&self) -> Result { + fn device_id(&self) -> Result { Ok("fake_device_id".to_string()) } } diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 61a7422f17..d521e26f4d 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -33,8 +33,7 @@ collab-document = { workspace = true, optional = true } collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } client-api = { workspace = true, optional = true } -tantivy = { workspace = true, optional = true } -uuid.workspace = true +tantivy = { version = "0.22.0", optional = true } [features] default = ["impl_from_dispatch_error", "impl_from_serde", "impl_from_reqwest", "impl_from_sqlite"] @@ -55,6 +54,8 @@ impl_from_tantivy = ["tantivy"] impl_from_sqlite = ["flowy-sqlite", "r2d2"] impl_from_appflowy_cloud = ["client-api"] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = ["flowy-codegen/ts"] [build-dependencies] flowy-codegen = { workspace = true, features = ["proto_gen"] } diff --git a/frontend/rust-lib/flowy-error/build.rs b/frontend/rust-lib/flowy-error/build.rs index 8dfda67156..81f0556ae3 100644 --- a/frontend/rust-lib/flowy-error/build.rs +++ b/frontend/rust-lib/flowy-error/build.rs @@ -1,4 +1,18 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 4112883e61..e97393d094 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -302,8 +302,8 @@ pub enum ErrorCode { #[error("Unsupported file format")] UnsupportedFileFormat = 104, - #[error("AppFlowy LAI not ready")] - AppFlowyLAINotReady = 105, + #[error("AI offline not started")] + AIOfflineNotInstalled = 105, #[error("Invalid Request")] InvalidRequest = 106, @@ -357,7 +357,7 @@ pub enum ErrorCode { #[error("Requested namespace has one or more invalid characters")] CustomNamespaceInvalidCharacter = 122, - #[error("AI Service is unavailable")] + #[error("Requested namespace has one or more invalid characters")] AIServiceUnavailable = 123, #[error("AI Image Response limit exceeded")] @@ -371,18 +371,6 @@ pub enum ErrorCode { #[error("Request timeout")] RequestTimeout = 127, - - #[error("Local AI is not ready")] - LocalAINotReady = 128, - - #[error("MCP error")] - MCPError = 129, - - #[error("Local AI disabled")] - LocalAIDisabled = 130, - - #[error("User not login")] - UserNotLogin = 131, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index a9a2b6fa2b..e7b6fdd439 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -95,14 +95,6 @@ impl FlowyError { self.code == ErrorCode::AIImageResponseLimitExceeded } - pub fn is_local_ai_not_ready(&self) -> bool { - self.code == ErrorCode::LocalAINotReady - } - - pub fn is_local_ai_disabled(&self) -> bool { - self.code == ErrorCode::LocalAIDisabled - } - pub fn is_ai_max_required(&self) -> bool { self.code == ErrorCode::AIMaxRequired } @@ -159,9 +151,6 @@ impl FlowyError { static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded); static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); - static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); - static_flowy_error!(local_ai_disabled, ErrorCode::LocalAIDisabled); - static_flowy_error!(user_not_login, ErrorCode::UserNotLogin); } impl std::convert::From for FlowyError { @@ -257,9 +246,3 @@ impl From for FlowyError { } } } - -impl From for FlowyError { - fn from(value: uuid::Error) -> Self { - FlowyError::internal().with_context(value) - } -} diff --git a/frontend/rust-lib/flowy-error/src/impl_from/database.rs b/frontend/rust-lib/flowy-error/src/impl_from/database.rs index 077ff2b708..3a72a7cdf3 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/database.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/database.rs @@ -1,12 +1,8 @@ use crate::FlowyError; -use flowy_sqlite::Error; impl std::convert::From for FlowyError { fn from(error: flowy_sqlite::Error) -> Self { - match error { - Error::NotFound => FlowyError::record_not_found(), - _ => FlowyError::internal().with_context(error), - } + FlowyError::internal().with_context(error) } } diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 05cc8f867d..3e7776d0bf 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -11,6 +11,22 @@ use uuid::Uuid; /// [FolderCloudService] represents the cloud service for folder. #[async_trait] pub trait FolderCloudService: Send + Sync + 'static { + /// Creates a new workspace for the user. + /// Returns error if the cloud service doesn't support multiple workspaces + async fn create_workspace(&self, uid: i64, name: &str) -> Result; + + async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>; + + /// Returns all workspaces of the user. + /// Returns vec![] if the cloud service doesn't support multiple workspaces + async fn get_all_workspace(&self) -> Result, FlowyError>; + + async fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> Result, FlowyError>; + async fn get_folder_snapshots( &self, workspace_id: &str, @@ -19,21 +35,21 @@ pub trait FolderCloudService: Send + Sync + 'static { async fn get_folder_doc_state( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, collab_type: CollabType, - object_id: &Uuid, + object_id: &str, ) -> Result, FlowyError>; async fn full_sync_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, params: FullSyncCollabParams, ) -> Result<(), FlowyError>; async fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec, ) -> Result<(), FlowyError>; @@ -41,64 +57,64 @@ pub trait FolderCloudService: Send + Sync + 'static { async fn publish_view( &self, - workspace_id: &Uuid, + workspace_id: &str, payload: Vec, ) -> Result<(), FlowyError>; async fn unpublish_views( &self, - workspace_id: &Uuid, - view_ids: Vec, + workspace_id: &str, + view_ids: Vec, ) -> Result<(), FlowyError>; - async fn get_publish_info(&self, view_id: &Uuid) -> Result; + async fn get_publish_info(&self, view_id: &str) -> Result; async fn set_publish_name( &self, - workspace_id: &Uuid, - view_id: Uuid, + workspace_id: &str, + view_id: String, new_name: String, ) -> Result<(), FlowyError>; async fn set_publish_namespace( &self, - workspace_id: &Uuid, + workspace_id: &str, new_namespace: String, ) -> Result<(), FlowyError>; async fn list_published_views( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError>; async fn get_default_published_view_info( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result; async fn set_default_published_view( &self, - workspace_id: &Uuid, + workspace_id: &str, view_id: uuid::Uuid, ) -> Result<(), FlowyError>; - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; + async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError>; - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result; + async fn get_publish_namespace(&self, workspace_id: &str) -> Result; async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError>; } #[derive(Debug)] pub struct FolderCollabParams { - pub object_id: Uuid, + pub object_id: String, pub encoded_collab_v1: Vec, pub collab_type: CollabType, } #[derive(Debug)] pub struct FullSyncCollabParams { - pub object_id: Uuid, + pub object_id: String, pub encoded_collab: EncodedCollab, pub collab_type: CollabType, } diff --git a/frontend/rust-lib/flowy-folder-pub/src/query.rs b/frontend/rust-lib/flowy-folder-pub/src/query.rs index 74761e44db..7b4682885d 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/query.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/query.rs @@ -3,7 +3,6 @@ use collab_entity::CollabType; use collab_folder::ViewLayout; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub struct QueryCollab { pub collab_type: CollabType, @@ -18,14 +17,14 @@ pub trait FolderQueryService: Send + Sync + 'static { /// the provided view layout, given that the parent view is not a space async fn get_surrounding_view_ids_with_view_layout( &self, - parent_view_id: &Uuid, + parent_view_id: &str, view_layout: ViewLayout, - ) -> Vec; + ) -> Vec; - async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option; + async fn get_collab(&self, object_id: &str, collab_type: CollabType) -> Option; } #[async_trait] pub trait FolderViewEdit: Send + Sync + 'static { - async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()>; + async fn set_view_title_if_empty(&self, view_id: &str, title: &str) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 13b19e48b8..30d9850e55 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -14,7 +14,6 @@ collab-plugins = { workspace = true } collab-integrate = { workspace = true } flowy-folder-pub = { workspace = true } flowy-search-pub = { workspace = true } -flowy-user-pub = { workspace = true } flowy-sqlite = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } @@ -42,7 +41,7 @@ validator.workspace = true async-trait.workspace = true client-api = { workspace = true } regex = "1.9.5" -futures = "0.3.31" +futures = "0.3.30" dashmap.workspace = true @@ -51,4 +50,6 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] test_helper = [] diff --git a/frontend/rust-lib/flowy-folder/build.rs b/frontend/rust-lib/flowy-folder/build.rs index 77c0c8125b..e9230d3d6d 100644 --- a/frontend/rust-lib/flowy-folder/build.rs +++ b/frontend/rust-lib/flowy-folder/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/import.rs b/frontend/rust-lib/flowy-folder/src/entities/import.rs index 83e8bdf874..4189dfaa6d 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/import.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/import.rs @@ -4,8 +4,6 @@ use crate::share::{ImportData, ImportItem, ImportParams, ImportType}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::FlowyError; use lib_infra::validator_fn::required_not_empty_str; -use std::str::FromStr; -use uuid::Uuid; use validator::Validate; #[derive(Clone, Debug, ProtoBuf_Enum)] @@ -78,8 +76,6 @@ impl TryInto for ImportPayloadPB { .map_err(|_| FlowyError::invalid_view_id())? .0; - let parent_view_id = Uuid::from_str(&parent_view_id)?; - let items = self .items .into_iter() diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 171bc39c7d..a8f331a91c 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -1,15 +1,12 @@ use collab_folder::{View, ViewIcon, ViewLayout}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::ErrorCode; -use flowy_folder_pub::cloud::gen_view_id; -use lib_infra::validator_fn::required_not_empty_str; use std::collections::HashMap; use std::convert::TryInto; use std::ops::{Deref, DerefMut}; -use std::str::FromStr; use std::sync::Arc; -use uuid::Uuid; -use validator::Validate; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; +use flowy_folder_pub::cloud::gen_view_id; use crate::entities::icon::ViewIconPB; use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail}; @@ -325,10 +322,10 @@ pub struct CreateOrphanViewPayloadPB { #[derive(Debug, Clone)] pub struct CreateViewParams { - pub parent_view_id: Uuid, + pub parent_view_id: String, pub name: String, pub layout: ViewLayoutPB, - pub view_id: Uuid, + pub view_id: String, pub initial_data: ViewData, pub meta: HashMap, // Mark the view as current view after creation. @@ -349,13 +346,9 @@ impl TryInto for CreateViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; - let parent_view_id = ViewIdentify::parse(self.parent_view_id) - .and_then(|id| Uuid::from_str(&id.0).map_err(|_| ErrorCode::InvalidParams))?; + let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0; // if view_id is not provided, generate a new view_id - let view_id = self - .view_id - .and_then(|v| Uuid::parse_str(&v).ok()) - .unwrap_or_else(gen_view_id); + let view_id = self.view_id.unwrap_or_else(|| gen_view_id().to_string()); Ok(CreateViewParams { parent_view_id, @@ -378,13 +371,13 @@ impl TryInto for CreateOrphanViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; - let view_id = Uuid::parse_str(&self.view_id).map_err(|_| ErrorCode::InvalidParams)?; + let parent_view_id = ViewIdentify::parse(self.view_id.clone())?.0; Ok(CreateViewParams { - parent_view_id: view_id, + parent_view_id, name, layout: self.layout, - view_id, + view_id: self.view_id, initial_data: ViewData::Data(self.initial_data.into()), meta: Default::default(), set_as_current: false, @@ -396,10 +389,9 @@ impl TryInto for CreateOrphanViewPayloadPB { } } -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +#[derive(Default, ProtoBuf, Clone, Debug)] pub struct ViewIdPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] pub value: String, } @@ -572,9 +564,9 @@ impl TryInto for MoveViewPayloadPB { #[derive(Debug)] pub struct MoveNestedViewParams { - pub view_id: Uuid, - pub new_parent_id: Uuid, - pub prev_view_id: Option, + pub view_id: String, + pub new_parent_id: String, + pub prev_view_id: Option, pub from_section: Option, pub to_section: Option, } @@ -583,20 +575,9 @@ impl TryInto for MoveNestedViewPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { - let view_id = Uuid::from_str(&ViewIdentify::parse(self.view_id)?.0) - .map_err(|_| ErrorCode::InvalidParams)?; - + let view_id = ViewIdentify::parse(self.view_id)?.0; let new_parent_id = ViewIdentify::parse(self.new_parent_id)?.0; - let new_parent_id = Uuid::from_str(&new_parent_id).map_err(|_| ErrorCode::InvalidParams)?; - - let prev_view_id = match self.prev_view_id { - Some(prev_view_id) => Some( - Uuid::from_str(&ViewIdentify::parse(prev_view_id)?.0) - .map_err(|_| ErrorCode::InvalidParams)?, - ), - None => None, - }; - + let prev_view_id = self.prev_view_id; Ok(MoveNestedViewParams { view_id, new_parent_id, diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 72e50562f3..21ff046226 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -134,7 +134,7 @@ impl TryInto for GetWorkspaceViewPB { } #[derive(Default, ProtoBuf, Debug, Clone)] -pub struct WorkspaceLatestPB { +pub struct WorkspaceSettingPB { #[pb(index = 1)] pub workspace_id: String, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 809651a262..30cbd29d1c 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -1,9 +1,8 @@ -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use std::str::FromStr; use std::sync::{Arc, Weak}; use tracing::instrument; -use uuid::Uuid; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; use crate::manager::FolderManager; @@ -18,6 +17,28 @@ fn upgrade_folder( Ok(folder) } +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn create_workspace_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let params: CreateWorkspaceParams = data.into_inner().try_into()?; + let workspace = folder.create_workspace(params).await?; + let views = folder + .get_views_belong_to(&workspace.id) + .await? + .into_iter() + .map(|view| view_pb_without_child_views(view.as_ref().clone())) + .collect::>(); + data_result_ok(WorkspacePB { + id: workspace.id, + name: workspace.name, + views, + create_time: workspace.created_at, + }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_all_workspace_handler( _data: AFPluginData, @@ -62,7 +83,7 @@ pub(crate) async fn read_private_views_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; let setting = folder.get_workspace_setting_pb().await?; data_result_ok(setting) @@ -111,7 +132,7 @@ pub(crate) async fn get_view_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let view_id = data.try_into_inner()?; + let view_id: ViewIdPB = data.into_inner(); let view_pb = folder.get_view_pb(&view_id.value).await?; data_result_ok(view_pb) } @@ -422,12 +443,7 @@ pub(crate) async fn unpublish_views_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params = data.into_inner(); - let view_ids = params - .view_ids - .into_iter() - .flat_map(|id| Uuid::from_str(&id).ok()) - .collect::>(); - folder.unpublish_views(view_ids).await?; + folder.unpublish_views(params.view_ids).await?; Ok(()) } @@ -438,7 +454,6 @@ pub(crate) async fn get_publish_info_handler( ) -> DataResult { let folder = upgrade_folder(folder)?; let view_id = data.into_inner().value; - let view_id = Uuid::from_str(&view_id)?; let info = folder.get_publish_info(&view_id).await?; data_result_ok(PublishInfoResponsePB::from(info)) } @@ -450,7 +465,6 @@ pub(crate) async fn set_publish_name_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let SetPublishNamePB { view_id, new_name } = data.into_inner(); - let view_id = Uuid::from_str(&view_id)?; folder.set_publish_name(view_id, new_name).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index c857353c4b..abd74bd338 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -11,6 +11,7 @@ use crate::manager::FolderManager; pub fn init(folder: Weak) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace + .event(FolderEvent::CreateFolderWorkspace, create_workspace_handler) .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) @@ -59,11 +60,12 @@ pub fn init(folder: Weak) -> AFPlugin { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum FolderEvent { - /// Deprecated: Create a new workspace + /// Create a new workspace + #[event(input = "CreateWorkspacePayloadPB", output = "WorkspacePB")] CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace - #[event(output = "WorkspaceLatestPB")] + #[event(output = "WorkspaceSettingPB")] GetCurrentWorkspaceSetting = 1, /// Return a list of workspaces that the current user can access. diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 37533ae500..b9e41f2998 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,9 +1,9 @@ use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, - CreateViewParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, - RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewLayoutPB, ViewPB, - ViewSectionPB, WorkspaceLatestPB, WorkspacePB, + CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, + MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, + ViewLayoutPB, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -44,18 +44,16 @@ use flowy_sqlite::kv::KVStorePreferences; use futures::future; use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use std::str::FromStr; use std::sync::{Arc, Weak}; use tokio::sync::RwLockWriteGuard; use tracing::{error, info, instrument}; -use uuid::Uuid; pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult; + fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &str) -> FlowyResult; } pub struct FolderManager { @@ -113,7 +111,7 @@ impl FolderManager { Ok::(workspace) }; - match folder.get_workspace_info(&workspace_id.to_string()) { + match folder.get_workspace_info(&workspace_id) { None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")), Some(workspace) => workspace_pb_from_workspace(workspace, &folder), } @@ -129,14 +127,14 @@ impl FolderManager { .ok_or_else(|| internal_error("The folder is not initialized"))? .read() .await - .get_folder_data(&workspace_id.to_string()) + .get_folder_data(&workspace_id) .ok_or_else(|| internal_error("Workspace id not match the id in current folder"))?; Ok(data) } pub async fn gather_publish_encode_collab( &self, - view_id: &Uuid, + view_id: &str, layout: &ViewLayout, ) -> FlowyResult { let handler = self.get_handler(layout)?; @@ -179,7 +177,7 @@ impl FolderManager { pub(crate) async fn make_folder>>( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak, data_source: Option, folder_notifier: T, @@ -189,7 +187,8 @@ impl FolderManager { let config = CollabBuilderConfig::default().sync_enable(true); let data_source = data_source.unwrap_or_else(|| { - CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source() + CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string()) + .into_data_source() }); let object_id = workspace_id; @@ -219,11 +218,8 @@ impl FolderManager { "Clear the folder data and try to open the folder again due to: {}", err ); - if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { - let _ = db - .delete_doc(uid, &workspace_id.to_string(), &object_id.to_string()) - .await; + let _ = db.delete_doc(uid, workspace_id, workspace_id).await; } Err(err.into()) }, @@ -233,7 +229,7 @@ impl FolderManager { pub(crate) async fn create_folder_with_data( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak, notifier: Option, folder_data: Option, @@ -244,8 +240,8 @@ impl FolderManager { .collab_builder .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source(); + let doc_state = CollabPersistenceImpl::new(collab_db.clone(), uid, workspace_id.to_string()) + .into_data_source(); let folder = self .collab_builder .create_folder( @@ -262,20 +258,16 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. - #[tracing::instrument(skip_all, err)] - pub async fn initialize_after_sign_in( - &self, - user_id: i64, - data_source: FolderInitDataSource, - ) -> FlowyResult<()> { + #[tracing::instrument(skip(self, user_id), err)] + pub async fn initialize_with_workspace_id(&self, user_id: i64) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; - if let Err(err) = self.initialize(user_id, &workspace_id, data_source).await { - // If failed to open folder with remote data, open from local disk. After open from the local - // disk. the data will be synced to the remote server. - error!( - "initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", - user_id, workspace_id, err - ); + let object_id = &workspace_id; + + let is_exist = self + .user + .is_folder_exist_on_disk(user_id, &workspace_id) + .unwrap_or(false); + if is_exist { self .initialize( user_id, @@ -285,29 +277,47 @@ impl FolderManager { }, ) .await?; + } else { + let folder_doc_state = self + .cloud_service + .get_folder_doc_state(&workspace_id, user_id, CollabType::Folder, object_id) + .await?; + if let Err(err) = self + .initialize( + user_id, + &workspace_id, + FolderInitDataSource::Cloud(folder_doc_state), + ) + .await + { + // If failed to open folder with remote data, open from local disk. After open from the local + // disk. the data will be synced to the remote server. + error!("initialize folder with error {:?}, fallback local", err); + self + .initialize( + user_id, + &workspace_id, + FolderInitDataSource::LocalDisk { + create_if_not_exist: false, + }, + ) + .await?; + } } Ok(()) } - pub async fn initialize_after_open_workspace( - &self, - uid: i64, - data_source: FolderInitDataSource, - ) -> FlowyResult<()> { - self.initialize_after_sign_in(uid, data_source).await - } - /// Initialize the folder for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user. #[instrument(level = "info", skip_all, err)] - pub async fn initialize_after_sign_up( + pub async fn initialize_with_new_user( &self, user_id: i64, _token: &str, is_new: bool, data_source: FolderInitDataSource, - workspace_id: &Uuid, + workspace_id: &str, ) -> FlowyResult<()> { // Create the default workspace if the user is new info!("initialize_when_sign_up: is_new: {}", is_new); @@ -353,11 +363,21 @@ impl FolderManager { /// pub async fn clear(&self, _user_id: i64) {} - pub async fn get_workspace_setting_pb(&self) -> FlowyResult { + #[tracing::instrument(level = "info", skip_all, err)] + pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult { + let uid = self.user.user_id()?; + let new_workspace = self + .cloud_service + .create_workspace(uid, ¶ms.name) + .await?; + Ok(new_workspace) + } + + pub async fn get_workspace_setting_pb(&self) -> FlowyResult { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; - Ok(WorkspaceLatestPB { - workspace_id: workspace_id.to_string(), + Ok(WorkspaceSettingPB { + workspace_id, latest_view, }) } @@ -475,7 +495,7 @@ impl FolderManager { .ok_or_else(|| FlowyError::internal().with_context("folder is not initialized"))?; let folder = lock.read().await; let workspace = folder - .get_workspace_info(&workspace_id.to_string()) + .get_workspace_info(&workspace_id) .ok_or_else(|| FlowyError::record_not_found().with_context("Can not find the workspace"))?; let views = folder @@ -586,9 +606,8 @@ impl FolderManager { // Drop the folder lock explicitly to avoid deadlock when following calls contains 'self' drop(folder); - let view_id = Uuid::from_str(view_id)?; let handler = self.get_handler(&view.layout)?; - handler.close_view(&view_id).await?; + handler.close_view(view_id).await?; } } Ok(()) @@ -825,28 +844,24 @@ impl FolderManager { let prev_view_id = params.prev_view_id; let from_section = params.from_section; let to_section = params.to_section; - let view = self.get_view_pb(&view_id.to_string()).await?; + let view = self.get_view_pb(&view_id).await?; // if the view is locked, the view can't be moved if view.is_locked.unwrap_or(false) { return Err(FlowyError::view_is_locked()); } - let old_parent_id = Uuid::from_str(&view.parent_view_id)?; + let old_parent_id = view.parent_view_id; if let Some(lock) = self.mutex_folder.load_full() { let mut folder = lock.write().await; - folder.move_nested_view( - &view_id.to_string(), - &new_parent_id.to_string(), - prev_view_id.map(|s| s.to_string()), - ); + folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); if from_section != to_section { if to_section == Some(ViewSectionPB::Private) { - folder.add_private_view_ids(vec![view_id.to_string()]); + folder.add_private_view_ids(vec![view_id.clone()]); } else { - folder.delete_private_view_ids(vec![view_id.to_string()]); + folder.delete_private_view_ids(vec![view_id.clone()]); } } - notify_parent_view_did_change(workspace_id, &folder, vec![new_parent_id, old_parent_id]); + notify_parent_view_did_change(&workspace_id, &folder, vec![new_parent_id, old_parent_id]); } Ok(()) } @@ -897,8 +912,7 @@ impl FolderManager { if let Some(lock) = self.mutex_folder.load_full() { let mut folder = lock.write().await; folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); - let parent_view_id = Uuid::from_str(&parent_view_id)?; - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); + notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]); } } } @@ -1101,8 +1115,7 @@ impl FolderManager { view.name, view.layout ); - let view_id = Uuid::from_str(&view.id)?; - let view_data = handler.duplicate_view(&view_id).await?; + let view_data = handler.duplicate_view(&view.id).await?; let index = self .get_view_relation(¤t_parent_id) @@ -1138,13 +1151,12 @@ impl FolderManager { view.name.clone() }; - let parent_view_id = Uuid::from_str(¤t_parent_id)?; let duplicate_params = CreateViewParams { - parent_view_id, + parent_view_id: current_parent_id.clone(), name, layout: view.layout.clone().into(), initial_data: ViewData::DuplicateData(view_data), - view_id: gen_view_id(), + view_id: gen_view_id().to_string(), meta: Default::default(), set_as_current: is_source_view && open_after_duplicated, index, @@ -1164,7 +1176,7 @@ impl FolderManager { if sync_after_create { if let Some(encoded_collab) = encoded_collab { - let object_id = Uuid::from_str(&duplicated_view.id)?; + let object_id = duplicated_view.id.clone(); let collab_type = match duplicated_view.layout { ViewLayout::Document => CollabType::Document, ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database, @@ -1196,20 +1208,20 @@ impl FolderManager { is_source_view = false } - let workspace_id = self.user.workspace_id()?; - let parent_view_id = Uuid::from_str(parent_view_id)?; + let workspace_id = &self.user.workspace_id()?; // Sync the view to the cloud if sync_after_create { self .cloud_service - .batch_create_folder_collab_objects(&workspace_id, objects) + .batch_create_folder_collab_objects(workspace_id, objects) .await?; } // notify the update here let folder = lock.read().await; - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); + notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id.to_string()]); + let duplicated_view = self.get_view_pb(&new_view_id).await?; Ok(duplicated_view) @@ -1230,7 +1242,6 @@ impl FolderManager { let view_layout: ViewLayout = view.layout.clone().into(); if let Some(handle) = self.operation_handlers.get(&view_layout) { info!("Open view: {}-{}", view.name, view.id); - let view_id = Uuid::from_str(&view.id)?; if let Err(err) = handle.open_view(&view_id).await { error!("Open view error: {:?}", err); } @@ -1238,8 +1249,8 @@ impl FolderManager { } let workspace_id = self.user.workspace_id()?; - let setting = WorkspaceLatestPB { - workspace_id: workspace_id.to_string(), + let setting = WorkspaceSettingPB { + workspace_id, latest_view: view, }; send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting); @@ -1356,18 +1367,18 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; self .cloud_service - .publish_view(&workspace_id, payload) + .publish_view(workspace_id.as_str(), payload) .await?; Ok(()) } /// Unpublish the view with the given view id. #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn unpublish_views(&self, view_ids: Vec) -> FlowyResult<()> { + pub async fn unpublish_views(&self, view_ids: Vec) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; self .cloud_service - .unpublish_views(&workspace_id, view_ids) + .unpublish_views(workspace_id.as_str(), view_ids) .await?; Ok(()) } @@ -1375,14 +1386,14 @@ impl FolderManager { /// Get the publish info of the view with the given view id. /// The publish info contains the namespace and publish_name of the view. #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_publish_info(&self, view_id: &Uuid) -> FlowyResult { + pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult { let publish_info = self.cloud_service.get_publish_info(view_id).await?; Ok(publish_info) } /// Sets the publish name of the view with the given view id. #[tracing::instrument(level = "debug", skip(self))] - pub async fn set_publish_name(&self, view_id: Uuid, new_name: String) -> FlowyResult<()> { + pub async fn set_publish_name(&self, view_id: String, new_name: String) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; self .cloud_service @@ -1398,7 +1409,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; self .cloud_service - .set_publish_namespace(&workspace_id, new_namespace) + .set_publish_namespace(workspace_id.as_str(), new_namespace) .await?; Ok(()) } @@ -1409,7 +1420,7 @@ impl FolderManager { let workspace_id = self.user.workspace_id()?; let namespace = self .cloud_service - .get_publish_namespace(&workspace_id) + .get_publish_namespace(workspace_id.as_str()) .await?; Ok(namespace) } @@ -1491,7 +1502,7 @@ impl FolderManager { }; if let Ok(payload) = self - .get_publish_payload(&Uuid::from_str(¤t_view_id)?, publish_name, layout) + .get_publish_payload(¤t_view_id, publish_name, layout) .await { payloads.push(payload); @@ -1540,7 +1551,7 @@ impl FolderManager { async fn get_publish_payload( &self, - view_id: &Uuid, + view_id: &str, publish_name: Option, layout: ViewLayout, ) -> FlowyResult { @@ -1548,20 +1559,18 @@ impl FolderManager { let encoded_collab_wrapper: GatherEncodedCollab = handler .gather_publish_encode_collab(&self.user, view_id) .await?; - - let view_str_id = view_id.to_string(); - let view = self.get_view_pb(&view_str_id).await?; + let view = self.get_view_pb(view_id).await?; let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); let child_views = self - .build_publish_views(&view_str_id) + .build_publish_views(view_id) .await .and_then(|v| v.child_views) .unwrap_or_default(); let ancestor_views = self - .get_view_ancestors_pb(&view_str_id) + .get_view_ancestors_pb(view_id) .await? .iter() .map(view_pb_to_publish_view) @@ -1711,9 +1720,8 @@ impl FolderManager { }; if let Some(view) = view { - let view_id = Uuid::from_str(view_id)?; if let Ok(handler) = self.get_handler(&view.layout) { - handler.delete_view(&view_id).await?; + handler.delete_view(view_id).await?; } } } @@ -1725,11 +1733,11 @@ impl FolderManager { #[instrument(level = "debug", skip_all, err)] pub(crate) async fn import_single_file( &self, - parent_view_id: Uuid, + parent_view_id: String, import_data: ImportItem, ) -> FlowyResult<(View, Vec<(String, CollabType, EncodedCollab)>)> { let handler = self.get_handler(&import_data.view_layout)?; - let view_id = gen_view_id(); + let view_id = gen_view_id().to_string(); let uid = self.user.user_id()?; let mut encoded_collab = vec![]; @@ -1737,7 +1745,7 @@ impl FolderManager { match import_data.data { ImportData::FilePath { file_path } => { handler - .import_from_file_path(&view_id.to_string(), &import_data.name, file_path) + .import_from_file_path(&view_id, &import_data.name, file_path) .await?; }, ImportData::Bytes { bytes } => { @@ -1791,18 +1799,16 @@ impl FolderManager { for data in import_data.items { // Import a single file and get the view and encoded collab data let (view, encoded_collabs) = self - .import_single_file(import_data.parent_view_id, data) + .import_single_file(import_data.parent_view_id.clone(), data) .await?; views.push(view_pb_without_child_views(view)); for (object_id, collab_type, encode_collab) in encoded_collabs { - if let Ok(object_id) = Uuid::from_str(&object_id) { - match self.get_folder_collab_params(object_id, collab_type, encode_collab) { - Ok(params) => objects.push(params), - Err(e) => { - error!("import error {}", e); - }, - } + match self.get_folder_collab_params(object_id, collab_type, encode_collab) { + Ok(params) => objects.push(params), + Err(e) => { + error!("import error {}", e); + }, } } } @@ -1816,7 +1822,7 @@ impl FolderManager { // Notify that the parent view has changed if let Some(lock) = self.mutex_folder.load_full() { let folder = lock.read().await; - notify_parent_view_did_change(workspace_id, &folder, vec![import_data.parent_view_id]); + notify_parent_view_did_change(&workspace_id, &folder, vec![import_data.parent_view_id]); } Ok(RepeatedViewPB { items: views }) @@ -1881,7 +1887,7 @@ impl FolderManager { fn get_folder_collab_params( &self, - object_id: Uuid, + object_id: String, collab_type: CollabType, encoded_collab: EncodedCollab, ) -> FlowyResult { @@ -1905,20 +1911,18 @@ impl FolderManager { let folder = lock.read().await; let view = folder.get_view(view_id)?; match folder.get_view(&view.parent_view_id) { - None => folder - .get_workspace_info(&workspace_id.to_string()) - .map(|workspace| { - ( - true, - workspace.id, - workspace - .child_views - .items - .into_iter() - .map(|view| view.id) - .collect::>(), - ) - }), + None => folder.get_workspace_info(&workspace_id).map(|workspace| { + ( + true, + workspace.id, + workspace + .child_views + .items + .into_iter() + .map(|view| view.id) + .collect::>(), + ) + }), Some(parent_view) => Some(( false, parent_view.id.clone(), @@ -2027,18 +2031,17 @@ impl FolderManager { .collect() } - pub async fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub fn remove_indices_for_workspace(&self, workspace_id: String) -> FlowyResult<()> { self .folder_indexer - .remove_indices_for_workspace(*workspace_id) - .await?; + .remove_indices_for_workspace(workspace_id)?; Ok(()) } } /// Return the views that belong to the workspace. The views are filtered by the trash and all the private views. -pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec { +pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) -> Vec { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2053,7 +2056,7 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder .map(|view| view.id) .collect::>(); - let mut views = folder.get_views_belong_to(&workspace_id.to_string()); + let mut views = folder.get_views_belong_to(workspace_id); // filter the views that are in the trash and all the private views views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); @@ -2071,15 +2074,20 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder /// Get all the child views belong to the view id, including the child views of the child views. fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec { - folder - .get_view_recursively(view_id) - .iter() + let child_view_ids = folder + .get_views_belong_to(view_id) + .into_iter() .map(|view| view.id.clone()) - .collect() + .collect::>(); + let mut all_child_view_ids = child_view_ids.clone(); + for child_view_id in child_view_ids { + all_child_view_ids.extend(get_all_child_view_ids(folder, &child_view_id)); + } + all_child_view_ids } /// Get the current private views of the user. -pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec { +pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder) -> Vec { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2094,7 +2102,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde .map(|view| view.id) .collect::>(); - let mut views = folder.get_views_belong_to(&workspace_id.to_string()); + let mut views = folder.get_views_belong_to(workspace_id); // filter the views that are in the trash and not in the private view ids views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); @@ -2111,7 +2119,6 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde } #[allow(clippy::large_enum_variant)] -#[derive(Debug)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index c581031f54..4393bfbb29 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -9,8 +9,7 @@ use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; use tokio::task::spawn_blocking; -use tracing::{error, event, info, Level}; -use uuid::Uuid; +use tracing::{event, info, Level}; impl FolderManager { /// Called immediately after the application launched if the user already sign in/sign up. @@ -18,7 +17,7 @@ impl FolderManager { pub async fn initialize( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, initial_data: FolderInitDataSource, ) -> FlowyResult<()> { // Update the workspace id @@ -38,6 +37,7 @@ impl FolderManager { ); } + let workspace_id = workspace_id.to_string(); // Get the collab db for the user with given user id. let collab_db = self.user.collab_db(uid)?; @@ -54,33 +54,33 @@ impl FolderManager { } => { let is_exist = self .user - .is_folder_exist_on_disk(uid, workspace_id) + .is_folder_exist_on_disk(uid, &workspace_id) .unwrap_or(false); // 1. if the folder exists, open it from local disk if is_exist { event!(Level::INFO, "Init folder from local disk"); self - .make_folder(uid, workspace_id, collab_db, None, folder_notifier) + .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) .await? } else if create_if_not_exist { // 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder // Currently, this branch is only used when the server type is supabase. For appflowy cloud, // the default workspace is already created when the user sign up. self - .create_default_folder(uid, workspace_id, collab_db, folder_notifier) + .create_default_folder(uid, &workspace_id, collab_db, folder_notifier) .await? } else { // 3. If the folder doesn't exist and create_if_not_exist is false, try to fetch the folder data from cloud/ // This will happen user can't fetch the folder data when the user sign in. let doc_state = self .cloud_service - .get_folder_doc_state(workspace_id, uid, CollabType::Folder, workspace_id) + .get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id) .await?; self .make_folder( uid, - workspace_id, + &workspace_id, collab_db.clone(), Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), @@ -92,14 +92,14 @@ impl FolderManager { if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self - .make_folder(uid, workspace_id, collab_db, None, folder_notifier) + .make_folder(uid, &workspace_id, collab_db, None, folder_notifier) .await? } else { event!(Level::INFO, "Restore folder from remote data"); self .make_folder( uid, - workspace_id, + &workspace_id, collab_db.clone(), Some(DataSource::DocStateV1(doc_state)), folder_notifier.clone(), @@ -115,46 +115,39 @@ impl FolderManager { let index_content_rx = folder.subscribe_index_content(); self .folder_indexer - .set_index_content_receiver(index_content_rx, *workspace_id) - .await; - self.handle_index_folder(*workspace_id, &folder).await; + .set_index_content_receiver(index_content_rx, workspace_id.clone()); + self.handle_index_folder(workspace_id.clone(), &folder); folder_state_rx }; self.mutex_folder.store(Some(folder.clone())); let weak_mutex_folder = Arc::downgrade(&folder); - subscribe_folder_sync_state_changed(*workspace_id, folder_state_rx, Arc::downgrade(&self.user)); + subscribe_folder_sync_state_changed( + workspace_id.clone(), + folder_state_rx, + Arc::downgrade(&self.user), + ); subscribe_folder_trash_changed( - *workspace_id, + workspace_id.clone(), section_change_rx, weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); subscribe_folder_view_changed( - *workspace_id, + workspace_id.clone(), view_rx, weak_mutex_folder.clone(), Arc::downgrade(&self.user), ); - let weak_folder_indexer = Arc::downgrade(&self.folder_indexer); - let workspace_id = *workspace_id; - tokio::spawn(async move { - if let Some(folder_indexer) = weak_folder_indexer.upgrade() { - if let Err(err) = folder_indexer.initialize(&workspace_id).await { - error!("Failed to initialize folder indexer: {:?}", err); - } - } - }); - Ok(()) } async fn create_default_folder( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak, folder_notifier: FolderNotify, ) -> Result>, FlowyError> { @@ -177,22 +170,24 @@ impl FolderManager { Ok(folder) } - async fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) { + fn handle_index_folder(&self, workspace_id: String, folder: &Folder) { let mut index_all = true; let encoded_collab = self .store_preferences - .get_object::(workspace_id.to_string().as_str()); + .get_object::(&workspace_id); if let Some(encoded_collab) = encoded_collab { if let Ok(changes) = folder.calculate_view_changes(encoded_collab) { let folder_indexer = self.folder_indexer.clone(); let views = folder.get_all_views(); + let wid = workspace_id.clone(); + if !changes.is_empty() && !views.is_empty() { spawn_blocking(move || { // We index the changes - folder_indexer.index_view_changes(views, changes, workspace_id); + folder_indexer.index_view_changes(views, changes, wid); }); index_all = false; } @@ -202,12 +197,15 @@ impl FolderManager { if index_all { let views = folder.get_all_views(); let folder_indexer = self.folder_indexer.clone(); - let _ = folder_indexer - .remove_indices_for_workspace(workspace_id) - .await; + let wid = workspace_id.clone(); + // We spawn a blocking task to index all views in the folder spawn_blocking(move || { - folder_indexer.index_all_views(views, workspace_id); + // We remove old indexes just in case + let _ = folder_indexer.remove_indices_for_workspace(wid.clone()); + + // We index all views from the workspace + folder_indexer.index_all_views(views, wid); }); } diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index 5d3034b5aa..dec4ff062d 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -13,16 +13,14 @@ use collab_folder::{ use lib_infra::sync_trace; use std::collections::HashSet; -use std::str::FromStr; use std::sync::Weak; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; use tracing::{event, trace, Level}; -use uuid::Uuid; /// Listen on the [ViewChange] after create/delete/update events happened pub(crate) fn subscribe_folder_view_changed( - workspace_id: Uuid, + workspace_id: String, mut rx: ViewChangeReceiver, weak_mutex_folder: Weak>, user: Weak, @@ -48,10 +46,9 @@ pub(crate) fn subscribe_folder_view_changed( ChildViewChangeReason::Create, ); let folder = lock.read().await; - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - sync_trace!("[Folder] create view: {:?}", view); - } + let parent_view_id = view.parent_view_id.clone(); + notify_parent_view_did_change(&workspace_id, &folder, vec![parent_view_id]); + sync_trace!("[Folder] create view: {:?}", view); }, ViewChange::DidDeleteView { views } => { for view in views { @@ -72,9 +69,7 @@ pub(crate) fn subscribe_folder_view_changed( ChildViewChangeReason::Update, ); let folder = lock.read().await; - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - } + notify_parent_view_did_change(&workspace_id, &folder, vec![view.parent_view_id]); }, }; } @@ -83,7 +78,7 @@ pub(crate) fn subscribe_folder_view_changed( } pub(crate) fn subscribe_folder_sync_state_changed( - workspace_id: Uuid, + workspace_id: String, mut folder_sync_state_rx: WatchStream, user: Weak, ) { @@ -98,19 +93,16 @@ pub(crate) fn subscribe_folder_sync_state_changed( } } - folder_notification_builder( - workspace_id.to_string(), - FolderNotification::DidUpdateFolderSyncUpdate, - ) - .payload(FolderSyncStatePB::from(state)) - .send(); + folder_notification_builder(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) + .payload(FolderSyncStatePB::from(state)) + .send(); } }); } /// Listen on the [TrashChange]s and notify the frontend some views were changed. pub(crate) fn subscribe_folder_trash_changed( - workspace_id: Uuid, + workspace_id: String, mut rx: SectionChangeReceiver, weak_mutex_folder: Weak>, user: Weak, @@ -139,9 +131,7 @@ pub(crate) fn subscribe_folder_trash_changed( let folder = lock.read().await; let views = folder.get_views(&ids); for view in views { - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - unique_ids.insert(parent_view_id); - } + unique_ids.insert(view.parent_view_id.clone()); } let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); @@ -150,7 +140,7 @@ pub(crate) fn subscribe_folder_trash_changed( .send(); let parent_view_ids = unique_ids.into_iter().collect(); - notify_parent_view_did_change(workspace_id, &folder, parent_view_ids); + notify_parent_view_did_change(&workspace_id, &folder, parent_view_ids); }, } } @@ -160,10 +150,10 @@ pub(crate) fn subscribe_folder_trash_changed( /// Notify the list of parent view ids that its child views were changed. #[tracing::instrument(level = "debug", skip(folder, parent_view_ids))] -pub(crate) fn notify_parent_view_did_change( - workspace_id: Uuid, +pub(crate) fn notify_parent_view_did_change>( + workspace_id: &str, folder: &Folder, - parent_view_ids: Vec, + parent_view_ids: Vec, ) -> Option<()> { let trash_ids = folder .get_all_trash_sections() @@ -172,23 +162,24 @@ pub(crate) fn notify_parent_view_did_change( .collect::>(); for parent_view_id in parent_view_ids { + let parent_view_id = parent_view_id.as_ref(); + // if the view's parent id equal to workspace id. Then it will fetch the current // workspace views. Because the workspace is not a view stored in the views map. if parent_view_id == workspace_id { - notify_did_update_workspace(&workspace_id, folder); - notify_did_update_section_views(&workspace_id, folder); + notify_did_update_workspace(workspace_id, folder); + notify_did_update_section_views(workspace_id, folder); } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. - let parent_view_id = parent_view_id.to_string(); - let parent_view = folder.get_view(&parent_view_id)?; - let mut child_views = folder.get_views_belong_to(&parent_view_id); + let parent_view = folder.get_view(parent_view_id)?; + let mut child_views = folder.get_views_belong_to(parent_view_id); child_views.retain(|view| !trash_ids.contains(&view.id)); event!(Level::DEBUG, child_views_count = child_views.len()); // Post the notification let parent_view_pb = view_pb_with_child_views(parent_view, child_views); - folder_notification_builder(&parent_view_id, FolderNotification::DidUpdateView) + folder_notification_builder(parent_view_id, FolderNotification::DidUpdateView) .payload(parent_view_pb) .send(); } @@ -197,7 +188,7 @@ pub(crate) fn notify_parent_view_did_change( None } -pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Folder) { +pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) { let public_views = get_workspace_public_view_pbs(workspace_id, folder); let private_views = get_workspace_private_view_pbs(workspace_id, folder); trace!( @@ -223,7 +214,7 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Fold .send(); } -pub(crate) fn notify_did_update_workspace(workspace_id: &Uuid, folder: &Folder) { +pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) { let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into(); folder_notification_builder(workspace_id, FolderNotification::DidUpdateWorkspaceViews) .payload(repeated_view) diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index 5629ef4133..8d9bfd5dea 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -1,7 +1,6 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; use lib_dispatch::prelude::ToBytes; -use tracing::trace; const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; @@ -69,14 +68,9 @@ impl std::convert::From for FolderNotification { } } -#[tracing::instrument(level = "trace", skip_all)] -pub(crate) fn folder_notification_builder( - id: T, - ty: FolderNotification, -) -> NotificationBuilder { - let id = id.to_string(); - trace!("folder_notification_builder: id = {id}, ty = {ty:?}"); - NotificationBuilder::new(&id, ty, FOLDER_OBSERVABLE_SOURCE) +#[tracing::instrument(level = "trace")] +pub(crate) fn folder_notification_builder(id: &str, ty: FolderNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, FOLDER_OBSERVABLE_SOURCE) } /// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the diff --git a/frontend/rust-lib/flowy-folder/src/share/import.rs b/frontend/rust-lib/flowy-folder/src/share/import.rs index 6fd8d8feab..2abac3540d 100644 --- a/frontend/rust-lib/flowy-folder/src/share/import.rs +++ b/frontend/rust-lib/flowy-folder/src/share/import.rs @@ -1,6 +1,5 @@ use collab_folder::ViewLayout; use std::fmt::{Display, Formatter}; -use uuid::Uuid; #[derive(Clone, Debug)] pub enum ImportType { @@ -36,6 +35,6 @@ impl Display for ImportData { #[derive(Clone, Debug)] pub struct ImportParams { - pub parent_view_id: Uuid, + pub parent_view_id: String, pub items: Vec, } diff --git a/frontend/rust-lib/flowy-folder/src/util.rs b/frontend/rust-lib/flowy-folder/src/util.rs index 98b87be52d..89d49f8a23 100644 --- a/frontend/rust-lib/flowy-folder/src/util.rs +++ b/frontend/rust-lib/flowy-folder/src/util.rs @@ -1,12 +1,11 @@ use crate::entities::UserFolderPB; use flowy_error::{ErrorCode, FlowyError}; -use uuid::Uuid; pub(crate) fn folder_not_init_error() -> FlowyError { FlowyError::internal().with_context("Folder not initialized") } -pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &Uuid) -> FlowyError { +pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError { FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB { uid, workspace_id: workspace_id.to_string(), diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 17919e07b1..2b0a9667c9 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -6,11 +6,11 @@ use collab_folder::hierarchy_builder::NestedViewBuilder; pub use collab_folder::View; use collab_folder::ViewLayout; use dashmap::DashMap; -use flowy_error::FlowyError; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use uuid::Uuid; + +use flowy_error::FlowyError; use lib_infra::util::timestamp; @@ -51,23 +51,23 @@ pub trait FolderOperationHandler: Send + Sync { Ok(()) } - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + async fn open_view(&self, view_id: &str) -> Result<(), FlowyError>; /// Closes the view and releases the resources that this view has in /// the backend - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + async fn close_view(&self, view_id: &str) -> Result<(), FlowyError>; /// Called when the view is deleted. /// This will called after the view is deleted from the trash. - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + async fn delete_view(&self, view_id: &str) -> Result<(), FlowyError>; /// Returns the [ViewData] that can be used to create the same view. - async fn duplicate_view(&self, view_id: &Uuid) -> Result; + async fn duplicate_view(&self, view_id: &str) -> Result; /// get the encoded collab data from the disk. async fn gather_publish_encode_collab( &self, _user: &Arc, - _view_id: &Uuid, + _view_id: &str, ) -> Result { Err(FlowyError::not_support()) } @@ -102,8 +102,8 @@ pub trait FolderOperationHandler: Send + Sync { async fn create_default_view( &self, user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, + parent_view_id: &str, + view_id: &str, name: &str, layout: ViewLayout, ) -> Result<(), FlowyError>; @@ -114,7 +114,7 @@ pub trait FolderOperationHandler: Send + Sync { async fn import_from_bytes( &self, uid: i64, - view_id: &Uuid, + view_id: &str, name: &str, import_type: ImportType, bytes: Vec, @@ -152,8 +152,8 @@ impl From for ViewLayout { pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout) -> View { let time = timestamp(); View { - id: params.view_id.to_string(), - parent_view_id: params.parent_view_id.to_string(), + id: params.view_id, + parent_view_id: params.parent_view_id, name: params.name, created_at: time, is_favorite: false, diff --git a/frontend/rust-lib/flowy-notification/Cargo.toml b/frontend/rust-lib/flowy-notification/Cargo.toml index 3851546541..b7a96898ff 100644 --- a/frontend/rust-lib/flowy-notification/Cargo.toml +++ b/frontend/rust-lib/flowy-notification/Cargo.toml @@ -25,3 +25,5 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-notification/build.rs b/frontend/rust-lib/flowy-notification/build.rs index 8dfda67156..81f0556ae3 100644 --- a/frontend/rust-lib/flowy-notification/build.rs +++ b/frontend/rust-lib/flowy-notification/build.rs @@ -1,4 +1,18 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-search-pub/Cargo.toml b/frontend/rust-lib/flowy-search-pub/Cargo.toml index 907942303d..631f2d2c83 100644 --- a/frontend/rust-lib/flowy-search-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-search-pub/Cargo.toml @@ -11,4 +11,4 @@ collab = { workspace = true } collab-folder = { workspace = true } flowy-error = { workspace = true } client-api = { workspace = true } -uuid.workspace = true \ No newline at end of file +futures = { workspace = true } diff --git a/frontend/rust-lib/flowy-search-pub/src/cloud.rs b/frontend/rust-lib/flowy-search-pub/src/cloud.rs index 8108cbed9a..f2ffb3c439 100644 --- a/frontend/rust-lib/flowy-search-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-search-pub/src/cloud.rs @@ -1,22 +1,12 @@ -pub use client_api::entity::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; +use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use uuid::Uuid; #[async_trait] pub trait SearchCloudService: Send + Sync + 'static { async fn document_search( &self, - workspace_id: &Uuid, + workspace_id: &str, query: String, ) -> Result, FlowyError>; - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec, - ) -> Result; } diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs index 4cc625af46..65e23a9ddb 100644 --- a/frontend/rust-lib/flowy-search-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -1,51 +1,47 @@ +use std::any::Any; use std::sync::Arc; use collab::core::collab::IndexContentReceiver; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout}; use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub struct IndexableData { pub id: String, pub data: String, pub icon: Option, pub layout: ViewLayout, - pub workspace_id: Uuid, + pub workspace_id: String, } impl IndexableData { - pub fn from_view(view: Arc, workspace_id: Uuid) -> Self { + pub fn from_view(view: Arc, workspace_id: String) -> Self { IndexableData { id: view.id.clone(), data: view.name.clone(), icon: view.icon.clone(), layout: view.layout.clone(), - workspace_id, + workspace_id: workspace_id.clone(), } } } -#[async_trait] pub trait IndexManager: Send + Sync { - async fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid); - async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; - async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; - async fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; - async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>; - async fn is_indexed(&self) -> bool; + fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: String); + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; + fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError>; + fn is_indexed(&self) -> bool; + + fn as_any(&self) -> &dyn Any; } -#[async_trait] pub trait FolderIndexManager: IndexManager { - async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; - - fn index_all_views(&self, views: Vec>, workspace_id: Uuid); - + fn index_all_views(&self, views: Vec>, workspace_id: String); fn index_view_changes( &self, views: Vec>, changes: Vec, - workspace_id: Uuid, + workspace_id: String, ); } diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index a803ad894f..2769f55479 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -11,16 +11,20 @@ collab-folder = { workspace = true } flowy-derive.workspace = true flowy-error = { workspace = true, features = [ - "impl_from_sqlite", - "impl_from_dispatch_error", - "impl_from_collab_document", - "impl_from_tantivy", - "impl_from_serde", + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_collab_document", + "impl_from_tantivy", + "impl_from_serde", ] } +flowy-notification.workspace = true +flowy-sqlite.workspace = true flowy-user.workspace = true flowy-search-pub.workspace = true flowy-folder = { workspace = true } + bytes.workspace = true +futures.workspace = true lib-dispatch.workspace = true lib-infra = { workspace = true } protobuf.workspace = true @@ -28,18 +32,24 @@ serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] } tracing.workspace = true -derive_builder.workspace = true + +async-stream = "0.3.4" strsim = "0.11.0" strum_macros = "0.26.1" -tantivy.workspace = true -uuid.workspace = true -allo-isolate = { version = "^0.1", features = ["catch-unwind"] } -futures.workspace = true -tokio-stream.workspace = true -async-stream = "0.3.6" +tantivy = { version = "0.22.0" } +tempfile = "3.9.0" +validator = { workspace = true, features = ["derive"] } + +diesel.workspace = true +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel_migrations = { version = "2.1.0", features = ["sqlite"] } [build-dependencies] flowy-codegen.workspace = true +[dev-dependencies] +tempfile = "3.10.0" + [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-search/build.rs b/frontend/rust-lib/flowy-search/build.rs index 77c0c8125b..2600d32fb7 100644 --- a/frontend/rust-lib/flowy-search/build.rs +++ b/frontend/rust-lib/flowy-search/build.rs @@ -1,7 +1,19 @@ +#[cfg(feature = "tauri_ts")] +use flowy_codegen::Project; + fn main() { + #[cfg(any(feature = "dart", feature = "tauri_ts"))] + let crate_name = env!("CARGO_PKG_NAME"); + #[cfg(feature = "dart")] { - flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); + flowy_codegen::protobuf_file::dart_gen(crate_name); + flowy_codegen::dart_event::gen(crate_name); + } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen(crate_name, crate_name, Project::Tauri); + flowy_codegen::ts_event::gen(crate_name, Project::Tauri); } } diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index 2127ef0d98..4f963033e0 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,23 +1,15 @@ -use crate::entities::{ - CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, - SearchResponsePB, SearchSourcePB, SearchSummaryPB, -}; +use std::sync::Arc; +use tracing::{trace, warn}; + +use flowy_error::FlowyResult; +use flowy_folder::{manager::FolderManager, ViewLayout}; +use flowy_search_pub::cloud::SearchCloudService; +use lib_infra::async_trait::async_trait; + use crate::{ - entities::{ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, + entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB}, services::manager::{SearchHandler, SearchType}, }; -use async_stream::stream; -use flowy_error::FlowyResult; -use flowy_folder::entities::ViewPB; -use flowy_folder::{manager::FolderManager, ViewLayout}; -use flowy_search_pub::cloud::{SearchCloudService, SearchResult}; -use lib_infra::async_trait::async_trait; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::Arc; -use tokio_stream::{self, Stream}; -use tracing::{trace, warn}; -use uuid::Uuid; pub struct DocumentSearchHandler { pub cloud_service: Arc, @@ -35,6 +27,7 @@ impl DocumentSearchHandler { } } } + #[async_trait] impl SearchHandler for DocumentSearchHandler { fn search_type(&self) -> SearchType { @@ -45,148 +38,64 @@ impl SearchHandler for DocumentSearchHandler { &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>> { - let cloud_service = self.cloud_service.clone(); - let folder_manager = self.folder_manager.clone(); + ) -> FlowyResult> { + let filter = match filter { + Some(filter) => filter, + None => return Ok(vec![]), + }; - Box::pin(stream! { - // Exit early if there is no filter. - let filter = if let Some(f) = filter { - f - } else { - yield Ok(CreateSearchResultPBArgs::default().build().unwrap()); - return; - }; + let workspace_id = match filter.workspace_id { + Some(workspace_id) => workspace_id, + None => return Ok(vec![]), + }; - // Parse workspace id. - let workspace_id = match Uuid::from_str(&filter.workspace_id) { - Ok(id) => id, - Err(e) => { - yield Err(e.into()); - return; - } - }; + let results = self + .cloud_service + .document_search(&workspace_id, query) + .await?; + trace!("[Search] remote search results: {:?}", results); - // Retrieve all available views. - let views = match folder_manager.get_all_views_pb().await { - Ok(views) => views, - Err(e) => { - yield Err(e); - return; - } - }; + // Grab all views from folder cache + // Notice that `get_all_view_pb` returns Views that don't include trashed and private views + let views = self.folder_manager.get_all_views_pb().await?; + let mut search_results: Vec = vec![]; - // Execute document search. - yield Ok( - CreateSearchResultPBArgs::default().searching(true) - .build() - .unwrap(), - ); - - let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await { - Ok(items) => items, - Err(e) => { - yield Err(e); - return; - } - }; - trace!("[Search] search result: {:?}", result_items); - - // Prepare input for search summary generation. - let summary_input: Vec = result_items - .iter() - .map(|v| SearchResult { - object_id: v.object_id, - content: v.content.clone(), - }) - .collect(); - - // Build search response items. - let mut items: Vec = Vec::new(); - for item in &result_items { - if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) { - items.push(SearchResponseItemPB { - id: item.object_id.to_string(), - display_name: view.name.clone(), - icon: extract_icon(view), - workspace_id: item.workspace_id.to_string(), - content: item.content.clone()} - ); - } else { - warn!("No view found for search result: {:?}", item); - } - } - - // Yield primary search result. - let search_result = RepeatedSearchResponseItemPB { items }; - yield Ok( - CreateSearchResultPBArgs::default() - .searching(false) - .search_result(Some(search_result)) - .generating_ai_summary(!result_items.is_empty()) - .build() - .unwrap(), - ); - - if result_items.is_empty() { - return; - } - - // Generate and yield search summary. - match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await { - Ok(summary_result) => { - trace!("[Search] search summary: {:?}", summary_result); - let summaries: Vec = summary_result - .summaries - .into_iter() - .map(|v| { - let sources: Vec = v.sources - .iter() - .flat_map(|id| { - views.iter().find(|v| v.id == id.to_string()).map(|view| SearchSourcePB { - id: id.to_string(), - display_name: view.name.clone(), - icon: extract_icon(view), - }) - }) - .collect(); - - SearchSummaryPB { content: v.content, sources, highlights: v.highlights } + for result in results { + if let Some(view) = views.iter().find(|v| v.id == result.object_id) { + // If there is no View for the result, we don't add it to the results + // If possible we will extract the icon to display for the result + let icon: Option = match view.icon.clone() { + Some(view_icon) => Some(ResultIconPB::from(view_icon)), + None => { + let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); + Some(ResultIconPB { + ty: ResultIconTypePB::Icon, + value: view_layout_ty.to_string(), }) - .collect(); + }, + }; - let summary_result = RepeatedSearchSummaryPB { items: summaries }; - yield Ok( - CreateSearchResultPBArgs::default() - .search_summary(Some(summary_result)) - .generating_ai_summary(false) - .build() - .unwrap(), - ); - } - Err(e) => { - warn!("Failed to generate search summary: {:?}", e); - yield Ok( - CreateSearchResultPBArgs::default() - .generating_ai_summary(false) - .build() - .unwrap(), - ); - } + search_results.push(SearchResultPB { + index_type: IndexTypePB::Document, + view_id: result.object_id.clone(), + id: result.object_id.clone(), + data: view.name.clone(), + icon, + score: result.score, + workspace_id: result.workspace_id, + preview: result.preview, + }); + } else { + warn!("No view found for search result: {:?}", result); } - }) - } -} + } -fn extract_icon(view: &ViewPB) -> Option { - match view.icon.clone() { - Some(view_icon) => Some(ResultIconPB::from(view_icon)), - None => { - let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); - Some(ResultIconPB { - ty: ResultIconTypePB::Icon, - value: view_layout_ty.to_string(), - }) - }, + trace!("[Search] showing results: {:?}", search_results); + Ok(search_results) + } + + /// Ignore for [DocumentSearchHandler] + fn index_count(&self) -> u64 { + 0 } } diff --git a/frontend/rust-lib/flowy-search/src/entities/index_type.rs b/frontend/rust-lib/flowy-search/src/entities/index_type.rs new file mode 100644 index 0000000000..77adc76a97 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/entities/index_type.rs @@ -0,0 +1,31 @@ +use flowy_derive::ProtoBuf_Enum; + +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum IndexTypePB { + View = 0, + Document = 1, + DocumentBlock = 2, + DatabaseRow = 3, +} + +impl Default for IndexTypePB { + fn default() -> Self { + Self::View + } +} + +impl std::convert::From for i32 { + fn from(notification: IndexTypePB) -> Self { + notification as i32 + } +} + +impl std::convert::From for IndexTypePB { + fn from(notification: i32) -> Self { + match notification { + 1 => IndexTypePB::View, + 2 => IndexTypePB::DocumentBlock, + _ => IndexTypePB::DatabaseRow, + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/entities/mod.rs b/frontend/rust-lib/flowy-search/src/entities/mod.rs index dc6aaace08..b4d7c682b9 100644 --- a/frontend/rust-lib/flowy-search/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-search/src/entities/mod.rs @@ -1,8 +1,10 @@ +mod index_type; mod notification; mod query; mod result; mod search_filter; +pub use index_type::*; pub use notification::*; pub use query::*; pub use result::*; diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index 4f12305d9a..a28ed2b5d8 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -1,13 +1,20 @@ -use super::SearchResponsePB; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use super::SearchResultPB; + #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchStatePB { - #[pb(index = 1, one_of)] - pub response: Option, +pub struct SearchResultNotificationPB { + #[pb(index = 1)] + pub items: Vec, #[pb(index = 2)] - pub search_id: String, + pub sends: u64, + + #[pb(index = 3, one_of)] + pub channel: Option, + + #[pb(index = 4)] + pub query: String, } #[derive(ProtoBuf_Enum, Debug, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/query.rs b/frontend/rust-lib/flowy-search/src/entities/query.rs index 65c92ebed0..8ffbcf3d46 100644 --- a/frontend/rust-lib/flowy-search/src/entities/query.rs +++ b/frontend/rust-lib/flowy-search/src/entities/query.rs @@ -13,9 +13,13 @@ pub struct SearchQueryPB { #[pb(index = 3, one_of)] pub filter: Option, - #[pb(index = 4)] - pub search_id: String, - - #[pb(index = 5)] - pub stream_port: i64, + /// Used to identify the channel of the search + /// + /// This can be used to have multiple search notification listeners in place. + /// It is up to the client to decide how to handle this. + /// + /// If not set, then no channel is used. + /// + #[pb(index = 4, one_of)] + pub channel: Option, } diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index a01f01b074..0f5ea4dc23 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -1,106 +1,55 @@ use collab_folder::{IconType, ViewIcon}; -use derive_builder::Builder; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_folder::entities::ViewIconPB; -#[derive(Debug, Default, ProtoBuf, Builder, Clone)] -#[builder(name = "CreateSearchResultPBArgs")] -#[builder(pattern = "mutable")] -pub struct SearchResponsePB { - #[pb(index = 1, one_of)] - #[builder(default)] - pub search_result: Option, +use super::IndexTypePB; - #[pb(index = 2, one_of)] - #[builder(default)] - pub search_summary: Option, - - #[pb(index = 3, one_of)] - #[builder(default)] - pub local_search_result: Option, - - #[pb(index = 4)] - #[builder(default)] - pub searching: bool, - - #[pb(index = 5)] - #[builder(default)] - pub generating_ai_summary: bool, +#[derive(Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedSearchResultPB { + #[pb(index = 1)] + pub items: Vec, } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedSearchSummaryPB { +pub struct SearchResultPB { #[pb(index = 1)] - pub items: Vec, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchSummaryPB { - #[pb(index = 1)] - pub content: String, + pub index_type: IndexTypePB, #[pb(index = 2)] - pub sources: Vec, + pub view_id: String, #[pb(index = 3)] - pub highlights: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchSourcePB { - #[pb(index = 1)] pub id: String, - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] - pub icon: Option, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedSearchResponseItemPB { - #[pb(index = 1)] - pub items: Vec, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchResponseItemPB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] - pub icon: Option, - #[pb(index = 4)] - pub workspace_id: String, + pub data: String, - #[pb(index = 5)] - pub content: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedLocalSearchResponseItemPB { - #[pb(index = 1)] - pub items: Vec, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct LocalSearchResponseItemPB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] + #[pb(index = 5, one_of)] pub icon: Option, - #[pb(index = 4)] + #[pb(index = 6)] + pub score: f64, + + #[pb(index = 7)] pub workspace_id: String, + + #[pb(index = 8, one_of)] + pub preview: Option, +} + +impl SearchResultPB { + pub fn with_score(&self, score: f64) -> Self { + SearchResultPB { + index_type: self.index_type.clone(), + view_id: self.view_id.clone(), + id: self.id.clone(), + data: self.data.clone(), + icon: self.icon.clone(), + score, + workspace_id: self.workspace_id.clone(), + preview: self.preview.clone(), + } + } } #[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs index 2059971a0d..33031b3b2c 100644 --- a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs +++ b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs @@ -2,6 +2,6 @@ use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] pub struct SearchFilterPB { - #[pb(index = 1)] - pub workspace_id: String, + #[pb(index = 1, one_of)] + pub workspace_id: Option, } diff --git a/frontend/rust-lib/flowy-search/src/event_handler.rs b/frontend/rust-lib/flowy-search/src/event_handler.rs index d79a719f6f..de611a078f 100644 --- a/frontend/rust-lib/flowy-search/src/event_handler.rs +++ b/frontend/rust-lib/flowy-search/src/event_handler.rs @@ -21,14 +21,7 @@ pub(crate) async fn search_handler( ) -> Result<(), FlowyError> { let query = data.into_inner(); let manager = upgrade_manager(manager)?; - manager - .perform_search( - query.search, - query.stream_port, - query.filter, - query.search_id, - ) - .await; + manager.perform_search(query.search, query.filter, query.channel); Ok(()) } diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index 1bb763b4a6..b3837668b8 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::entities::{LocalSearchResponseItemPB, ResultIconPB}; +use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB}; #[derive(Debug, Serialize, Deserialize)] pub struct FolderIndexData { @@ -11,7 +11,7 @@ pub struct FolderIndexData { pub workspace_id: String, } -impl From for LocalSearchResponseItemPB { +impl From for SearchResultPB { fn from(data: FolderIndexData) -> Self { let icon = if data.icon.is_empty() { None @@ -23,10 +23,14 @@ impl From for LocalSearchResponseItemPB { }; Self { + index_type: IndexTypePB::View, + view_id: data.id.clone(), id: data.id, - display_name: data.title, + data: data.title, + score: 0.0, icon, workspace_id: data.workspace_id, + preview: None, } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs index e21ce1c98c..f92e17cda1 100644 --- a/frontend/rust-lib/flowy-search/src/folder/handler.rs +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -1,14 +1,12 @@ -use super::indexer::FolderIndexManagerImpl; -use crate::entities::{ - CreateSearchResultPBArgs, RepeatedLocalSearchResponseItemPB, SearchFilterPB, SearchResponsePB, +use crate::{ + entities::{SearchFilterPB, SearchResultPB}, + services::manager::{SearchHandler, SearchType}, }; -use crate::services::manager::{SearchHandler, SearchType}; -use async_stream::stream; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; -use std::pin::Pin; use std::sync::Arc; -use tokio_stream::{self, Stream}; + +use super::indexer::FolderIndexManagerImpl; pub struct FolderSearchHandler { pub index_manager: Arc, @@ -30,26 +28,19 @@ impl SearchHandler for FolderSearchHandler { &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>> { - let index_manager = self.index_manager.clone(); + ) -> FlowyResult> { + let mut results = self.index_manager.search(query, filter.clone())?; + if let Some(filter) = filter { + if let Some(workspace_id) = filter.workspace_id { + // Filter results by workspace ID + results.retain(|result| result.workspace_id == workspace_id); + } + } - Box::pin(stream! { - // Perform search (if search() returns a Result) - let mut items = match index_manager.search(query).await { - Ok(items) => items, - Err(err) => { - yield Err(err); - return; - } - }; + Ok(results) + } - if let Some(filter) = filter { - items.retain(|result| result.workspace_id == filter.workspace_id); - } - - // Build the search result. - let search_result = RepeatedLocalSearchResponseItemPB {items}; - yield Ok(CreateSearchResultPBArgs::default().local_search_result(Some(search_result)).build().unwrap()) - }) + fn index_count(&self) -> u64 { + self.index_manager.num_docs() } } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 59622852d5..8c1d5633ac 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -1,124 +1,190 @@ -use super::entities::FolderIndexData; -use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB}; -use crate::folder::schema::{ - FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, - FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, +use std::{ + any::Any, + collections::HashMap, + fs, + ops::Deref, + path::Path, + sync::{Arc, Mutex, MutexGuard, Weak}, +}; + +use crate::{ + entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB}, + folder::schema::{ + FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, + FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, + }, }; use collab::core::collab::{IndexContent, IndexContentReceiver}; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexContent, ViewLayout}; use flowy_error::{FlowyError, FlowyResult}; use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; use flowy_user::services::authenticate_user::AuthenticateUser; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use std::{collections::HashMap, fs}; + +use strsim::levenshtein; use tantivy::{ collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, - Index, IndexReader, IndexWriter, TantivyDocument, TantivyError, Term, + Index, IndexReader, IndexWriter, TantivyDocument, Term, }; -use tokio::sync::RwLock; -use tracing::{error, info}; -use uuid::Uuid; -pub struct TantivyState { - pub path: PathBuf, - pub index: Index, - pub folder_schema: FolderSchema, - pub index_reader: IndexReader, - pub index_writer: IndexWriter, -} - -impl Drop for TantivyState { - fn drop(&mut self) { - tracing::trace!("Dropping TantivyState at {:?}", self.path); - } -} +use super::entities::FolderIndexData; #[derive(Clone)] pub struct FolderIndexManagerImpl { - auth_user: Weak, - state: Arc>>, + folder_schema: Option, + index: Option, + index_reader: Option, + index_writer: Option>>, } +const FOLDER_INDEX_DIR: &str = "folder_index"; + impl FolderIndexManagerImpl { - pub fn new(auth_user: Weak) -> Self { - Self { - auth_user, - state: Arc::new(RwLock::new(None)), - } - } - - async fn with_writer(&self, f: F) -> FlowyResult - where - F: FnOnce(&mut IndexWriter, &FolderSchema) -> FlowyResult, - { - let mut lock = self.state.write().await; - if let Some(ref mut state) = *lock { - f(&mut state.index_writer, &state.folder_schema) - } else { - Err(FlowyError::internal().with_context("Index not initialized. Call initialize first")) - } - } - - /// Initializes the state using the workspace directory. - async fn initialize(&self, workspace_id: &Uuid) -> FlowyResult<()> { - if let Some(state) = self.state.write().await.take() { - info!("Re-initializing folder indexer"); - drop(state); - } - - // Since the directory lock may not be immediately released, - // a workaround is implemented by waiting for 3 seconds before proceeding further. This delay helps - // to avoid errors related to trying to open an index directory while an IndexWriter is still active. - // - // Also, we don't need to initialize the indexer immediately. - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - - let auth_user = self - .auth_user - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?; - - let index_path = auth_user.get_index_path()?.join(workspace_id.to_string()); - if !index_path.exists() { - fs::create_dir_all(&index_path).map_err(|e| { - error!("Failed to create folder index directory: {:?}", e); - FlowyError::internal().with_context("Failed to create folder index") - })?; - } - - info!("Folder indexer initialized at: {:?}", index_path); - let folder_schema = FolderSchema::new(); - let dir = MmapDirectory::open(index_path.clone())?; - let index = Index::open_or_create(dir, folder_schema.schema.clone())?; - let index_reader = index.reader()?; - - let index_writer = match index.writer::<_>(50_000_000) { - Ok(index_writer) => index_writer, - Err(err) => { - if let TantivyError::LockFailure(_, _) = err { - error!( - "Failed to acquire lock for index writer: {:?}, retry later", - err - ); - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - } - index.writer::<_>(50_000_000)? + pub fn new(auth_user: Option>) -> Self { + let auth_user = match auth_user { + Some(auth_user) => auth_user, + None => { + return FolderIndexManagerImpl::empty(); }, }; - *self.state.write().await = Some(TantivyState { - path: index_path, - index, - folder_schema, - index_reader, - index_writer, - }); + // AuthenticateUser is required to get the index path + let authenticate_user = auth_user.upgrade(); + + // Storage path is the users data path with an index directory + // Eg. /usr/flowy-data/indexes + let storage_path = match authenticate_user { + Some(auth_user) => auth_user.get_index_path(), + None => { + tracing::error!("FolderIndexManager: AuthenticateUser is not available"); + return FolderIndexManagerImpl::empty(); + }, + }; + + // We check if the `folder_index` directory exists, if not we create it + let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR)); + if !index_path.exists() { + let res = fs::create_dir_all(&index_path); + if let Err(e) = res { + tracing::error!( + "FolderIndexManager failed to create index directory: {:?}", + e + ); + return FolderIndexManagerImpl::empty(); + } + } + + // The folder schema is used to define the fields of the index along + // with how they are stored and if the field is indexed + let folder_schema = FolderSchema::new(); + + // We open the existing or newly created folder_index directory + // This is required by the Tantivy Index, as it will use it to store + // and read index data + let index = match MmapDirectory::open(index_path) { + // We open or create an index that takes the directory r/w and the schema. + Ok(dir) => match Index::open_or_create(dir, folder_schema.schema.clone()) { + Ok(index) => index, + Err(e) => { + tracing::error!("FolderIndexManager failed to open index: {:?}", e); + return FolderIndexManagerImpl::empty(); + }, + }, + Err(e) => { + tracing::error!("FolderIndexManager failed to open index directory: {:?}", e); + return FolderIndexManagerImpl::empty(); + }, + }; + + // We only need one IndexReader per index + let index_reader = index.reader(); + let index_writer = index.writer(50_000_000); + + let (index_reader, index_writer) = match (index_reader, index_writer) { + (Ok(reader), Ok(writer)) => (reader, writer), + _ => { + tracing::error!("FolderIndexManager failed to instantiate index writer and/or reader"); + return FolderIndexManagerImpl::empty(); + }, + }; + + Self { + folder_schema: Some(folder_schema), + index: Some(index), + index_reader: Some(index_reader), + index_writer: Some(Arc::new(Mutex::new(index_writer))), + } + } + + fn index_all(&self, indexes: Vec) -> Result<(), FlowyError> { + if indexes.is_empty() { + return Ok(()); + } + + let mut index_writer = self.get_index_writer()?; + let folder_schema = self.get_folder_schema()?; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + let workspace_id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + for data in indexes { + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data.clone(), + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.clone(), + ]); + } + + index_writer.commit()?; Ok(()) } + pub fn num_docs(&self) -> u64 { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs()) + .unwrap_or(0) + } + + fn empty() -> Self { + Self { + folder_schema: None, + index: None, + index_reader: None, + index_writer: None, + } + } + + fn get_index_writer(&self) -> FlowyResult> { + match &self.index_writer { + Some(index_writer) => match index_writer.deref().lock() { + Ok(writer) => Ok(writer), + Err(e) => { + tracing::error!("FolderIndexManager failed to lock index writer: {:?}", e); + Err(FlowyError::folder_index_manager_unavailable()) + }, + }, + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + + fn get_folder_schema(&self) -> FlowyResult { + match &self.folder_schema { + Some(folder_schema) => Ok(folder_schema.clone()), + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + fn extract_icon( &self, view_icon: Option, @@ -134,99 +200,132 @@ impl FolderIndexManagerImpl { icon = Some(view_icon.value); } else { icon_ty = ResultIconTypePB::Icon.into(); - let layout_ty = view_layout as i64; + let layout_ty: i64 = view_layout.into(); icon = Some(layout_ty.to_string()); } + (icon, icon_ty) } - /// Simple implementation to index all given data by spawning async tasks. - fn index_all(&self, data_vec: Vec) -> Result<(), FlowyError> { - for data in data_vec { - let indexer = self.clone(); - tokio::spawn(async move { - let _ = indexer.add_index(data).await; - }); - } - Ok(()) - } + pub fn search( + &self, + query: String, + _filter: Option, + ) -> Result, FlowyError> { + let folder_schema = self.get_folder_schema()?; - /// Searches the index using the given query string. - pub async fn search(&self, query: String) -> Result, FlowyError> { - let lock = self.state.read().await; - let state = lock + let (index, index_reader) = self + .index .as_ref() + .zip(self.index_reader.as_ref()) .ok_or_else(FlowyError::folder_index_manager_unavailable)?; - let schema = &state.folder_schema; - let index = &state.index; - let reader = &state.index_reader; - let title_field = schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let mut parser = QueryParser::for_index(index, vec![title_field]); - parser.set_field_fuzzy(title_field, true, 2, true); + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let built_query = parser.parse_query(&query)?; - let searcher = reader.searcher(); + let length = query.len(); + let distance: u8 = if length >= 2 { 2 } else { 1 }; + + let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]); + query_parser.set_field_fuzzy(title_field, true, distance, true); + let built_query = query_parser.parse_query(&query.clone())?; + + let searcher = index_reader.searcher(); + let mut search_results: Vec = vec![]; let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; - - let mut results = Vec::new(); for (_score, doc_address) in top_docs { - let doc: TantivyDocument = searcher.doc(doc_address)?; - let named_doc = doc.to_named_doc(&schema.schema); + let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?; + let mut content = HashMap::new(); + let named_doc = retrieved_doc.to_named_doc(&folder_schema.schema); for (k, v) in named_doc.0 { content.insert(k, v[0].clone()); } - if !content.is_empty() { - let s = serde_json::to_string(&content)?; - let result: LocalSearchResponseItemPB = serde_json::from_str::(&s)?.into(); - results.push(result); + + if content.is_empty() { + continue; } + + let s = serde_json::to_string(&content)?; + let result: SearchResultPB = serde_json::from_str::(&s)?.into(); + let score = self.score_result(&query, &result.data); + search_results.push(result.with_score(score)); } - Ok(results) + Ok(search_results) + } + + // Score result by distance + fn score_result(&self, query: &str, term: &str) -> f64 { + let distance = levenshtein(query, term) as f64; + 1.0 / (distance + 1.0) + } + + fn get_schema_fields(&self) -> Result<(Field, Field, Field, Field, Field), FlowyError> { + let folder_schema = match self.folder_schema.clone() { + Some(schema) => schema, + _ => return Err(FlowyError::folder_index_manager_unavailable()), + }; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + let workspace_id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + Ok(( + id_field, + title_field, + icon_field, + icon_ty_field, + workspace_id_field, + )) } } -#[async_trait] impl IndexManager for FolderIndexManagerImpl { - async fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { + fn is_indexed(&self) -> bool { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs() > 0) + .unwrap_or(false) + } + + fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: String) { let indexer = self.clone(); - let wid = workspace_id; + let wid = workspace_id.clone(); tokio::spawn(async move { while let Ok(msg) = rx.recv().await { match msg { IndexContent::Create(value) => match serde_json::from_value::(value) { Ok(view) => { - let _ = indexer - .add_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }) - .await; + let _ = indexer.add_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid.clone(), + }); }, - Err(err) => tracing::error!("FolderIndexManager error deserialize (create): {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), }, IndexContent::Update(value) => match serde_json::from_value::(value) { Ok(view) => { - let _ = indexer - .update_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }) - .await; + let _ = indexer.update_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid.clone(), + }); }, - Err(err) => error!("FolderIndexManager error deserialize (update): {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), }, IndexContent::Delete(ids) => { - if let Err(e) = indexer.remove_indices(ids).await { - error!("FolderIndexManager error (delete): {:?}", e); + if let Err(e) = indexer.remove_indices(ids) { + tracing::error!("FolderIndexManager error deserialize: {:?}", e); } }, } @@ -234,107 +333,100 @@ impl IndexManager for FolderIndexManagerImpl { }); } - async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + self.get_schema_fields()?; + + let delete_term = Term::from_field_text(id_field, &data.id.clone()); + + // Remove old index + index_writer.delete_term(delete_term); + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - self - .with_writer(|index_writer, folder_schema| { - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - get_schema_fields(folder_schema)?; - let _ = index_writer.add_document(doc![ - id_field => data.id, - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - index_writer.commit()?; - Ok(()) - }) - .await?; + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.clone(), + ]); + + index_writer.commit()?; Ok(()) } - async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - get_schema_fields(folder_schema)?; - let delete_term = Term::from_field_text(id_field, &data.id); - index_writer.delete_term(delete_term); + fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; - let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - let _ = index_writer.add_document(doc![ - id_field => data.id, - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - - index_writer.commit()?; - Ok(()) - }) - .await?; - - Ok(()) - } - - async fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - for id in ids { - let delete_term = Term::from_field_text(id_field, &id); - index_writer.delete_term(delete_term); - } - - index_writer.commit()?; - Ok(()) - }) - .await?; - - Ok(()) - } - - async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - - let delete_term = Term::from_field_text(id_field, &workspace_id.to_string()); - index_writer.delete_term(delete_term); - index_writer.commit()?; - Ok(()) - }) - .await?; - Ok(()) - } - - async fn is_indexed(&self) -> bool { - let lock = self.state.read().await; - if let Some(ref state) = *lock { - state.index_reader.searcher().num_docs() > 0 - } else { - false + let folder_schema = self.get_folder_schema()?; + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + for id in ids { + let delete_term = Term::from_field_text(id_field, &id); + index_writer.delete_term(delete_term); } + + index_writer.commit()?; + + Ok(()) + } + + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + self.get_schema_fields()?; + + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id, + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id, + ]); + + index_writer.commit()?; + + Ok(()) + } + + /// Removes all indexes that are related by workspace id. This is useful + /// for cleaning indexes when eg. removing/leaving a workspace. + /// + fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let folder_schema = self.get_folder_schema()?; + let id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + let delete_term = Term::from_field_text(id_field, &workspace_id); + index_writer.delete_term(delete_term); + + index_writer.commit()?; + + Ok(()) + } + + fn as_any(&self) -> &dyn Any { + self } } -#[async_trait] impl FolderIndexManager for FolderIndexManagerImpl { - async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - self.initialize(workspace_id).await?; - Ok(()) - } - - fn index_all_views(&self, views: Vec>, workspace_id: Uuid) { + fn index_all_views(&self, views: Vec>, workspace_id: String) { let indexable_data = views .into_iter() - .map(|view| IndexableData::from_view(view, workspace_id)) + .map(|view| IndexableData::from_view(view, workspace_id.clone())) .collect(); + let _ = self.index_all(indexable_data); } @@ -342,56 +434,29 @@ impl FolderIndexManager for FolderIndexManagerImpl { &self, views: Vec>, changes: Vec, - workspace_id: Uuid, + workspace_id: String, ) { let mut views_iter = views.into_iter(); for change in changes { match change { FolderViewChange::Inserted { view_id } => { - if let Some(view) = views_iter.find(|view| view.id == view_id) { - let indexable_data = IndexableData::from_view(view, workspace_id); - let f = self.clone(); - tokio::spawn(async move { - let _ = f.add_index(indexable_data).await; - }); + let view = views_iter.find(|view| view.id == view_id); + if let Some(view) = view { + let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let _ = self.add_index(indexable_data); } }, FolderViewChange::Updated { view_id } => { - if let Some(view) = views_iter.find(|view| view.id == view_id) { - let indexable_data = IndexableData::from_view(view, workspace_id); - let f = self.clone(); - tokio::spawn(async move { - let _ = f.update_index(indexable_data).await; - }); + let view = views_iter.find(|view| view.id == view_id); + if let Some(view) = view { + let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let _ = self.update_index(indexable_data); } }, FolderViewChange::Deleted { view_ids } => { - let f = self.clone(); - tokio::spawn(async move { - let _ = f.remove_indices(view_ids).await; - }); + let _ = self.remove_indices(view_ids); }, - } + }; } } } - -fn get_schema_fields( - folder_schema: &FolderSchema, -) -> Result<(Field, Field, Field, Field, Field), FlowyError> { - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; - let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; - let workspace_id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - - Ok(( - id_field, - title_field, - icon_field, - icon_ty_field, - workspace_id_field, - )) -} diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index a71449d5d2..84659b3037 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -1,13 +1,12 @@ -use crate::entities::{SearchFilterPB, SearchResponsePB, SearchStatePB}; -use allo_isolate::Isolate; -use flowy_error::FlowyResult; -use lib_infra::async_trait::async_trait; -use lib_infra::isolate_stream::{IsolateSink, SinkExt}; use std::collections::HashMap; -use std::pin::Pin; use std::sync::Arc; -use tokio_stream::{self, Stream, StreamExt}; -use tracing::{error, trace}; + +use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner}; +use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB}; +use flowy_error::FlowyResult; + +use lib_infra::async_trait::async_trait; +use tokio::sync::broadcast; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum SearchType { @@ -20,12 +19,15 @@ pub trait SearchHandler: Send + Sync + 'static { /// returns the type of search this handler is responsible for fn search_type(&self) -> SearchType; - /// performs a search and returns a stream of results + /// performs a search and returns the results async fn perform_search( &self, query: String, filter: Option, - ) -> Pin> + Send + 'static>>; + ) -> FlowyResult>; + + /// returns the number of indexed objects + fn index_count(&self) -> u64; } /// The [SearchManager] is used to inject multiple [SearchHandler]'s @@ -34,7 +36,7 @@ pub trait SearchHandler: Send + Sync + 'static { /// pub struct SearchManager { pub handlers: HashMap>, - current_search: Arc>>, + notifier: SearchNotifier, } impl SearchManager { @@ -44,87 +46,45 @@ impl SearchManager { .map(|handler| (handler.search_type(), handler)) .collect(); - Self { - handlers, - current_search: Arc::new(tokio::sync::Mutex::new(None)), - } + // Initialize Search Notifier + let (notifier, _) = broadcast::channel(100); + tokio::spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run()); + + Self { handlers, notifier } } pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc> { self.handlers.get(&search_type) } - pub async fn perform_search( + pub fn perform_search( &self, query: String, - stream_port: i64, filter: Option, - search_id: String, + channel: Option, ) { - // Cancel previous search by updating current_search - *self.current_search.lock().await = Some(search_id.clone()); - + let max: usize = self.handlers.len(); let handlers = self.handlers.clone(); - let sink = IsolateSink::new(Isolate::new(stream_port)); - let mut join_handles = vec![]; - let current_search = self.current_search.clone(); - - tracing::info!("[Search] perform search: {}", query); for (_, handler) in handlers { - let mut clone_sink = sink.clone(); - let query = query.clone(); - let filter = filter.clone(); - let search_id = search_id.clone(); - let current_search = current_search.clone(); + let q = query.clone(); + let f = filter.clone(); + let ch = channel.clone(); + let notifier = self.notifier.clone(); - let handle = tokio::spawn(async move { - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] cancel search: {}", query); - return; - } + tokio::spawn(async move { + let res = handler.perform_search(q.clone(), f).await; - let mut stream = handler.perform_search(query.clone(), filter).await; - while let Some(Ok(search_result)) = stream.next().await { - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] discard search stream: {}", query); - return; - } + let items = res.unwrap_or_default(); - let resp = SearchStatePB { - response: Some(search_result), - search_id: search_id.clone(), - }; - if let Ok::, _>(data) = resp.try_into() { - if let Err(err) = clone_sink.send(data).await { - error!("Failed to send search result: {}", err); - break; - } - } - } - - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] discard search result: {}", query); - return; - } - - let resp = SearchStatePB { - response: None, - search_id: search_id.clone(), + let notification = SearchResultNotificationPB { + items, + sends: max as u64, + channel: ch, + query: q, }; - if let Ok::, _>(data) = resp.try_into() { - let _ = clone_sink.send(data).await; - } + + let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification)); }); - join_handles.push(handle); } - futures::future::join_all(join_handles).await; } } - -async fn is_current_search( - current_search: &Arc>>, - search_id: &str, -) -> bool { - let current = current_search.lock().await; - current.as_ref().map_or(false, |id| id == search_id) -} diff --git a/frontend/rust-lib/flowy-search/src/services/mod.rs b/frontend/rust-lib/flowy-search/src/services/mod.rs index ff8de9eb9a..2a417e6c62 100644 --- a/frontend/rust-lib/flowy-search/src/services/mod.rs +++ b/frontend/rust-lib/flowy-search/src/services/mod.rs @@ -1 +1,2 @@ pub mod manager; +pub mod notifier; diff --git a/frontend/rust-lib/flowy-search/src/services/notifier.rs b/frontend/rust-lib/flowy-search/src/services/notifier.rs new file mode 100644 index 0000000000..abbf5d4b0c --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/services/notifier.rs @@ -0,0 +1,61 @@ +use async_stream::stream; +use flowy_notification::NotificationBuilder; +use futures::stream::StreamExt; +use tokio::sync::broadcast; + +use crate::entities::{SearchNotification, SearchResultNotificationPB}; + +const SEARCH_OBSERVABLE_SOURCE: &str = "Search"; +const SEARCH_ID: &str = "SEARCH_IDENTIFIER"; + +#[derive(Clone)] +pub enum SearchResultChanged { + SearchResultUpdate(SearchResultNotificationPB), +} + +pub type SearchNotifier = broadcast::Sender; + +pub(crate) struct SearchResultReceiverRunner( + pub(crate) Option>, +); + +impl SearchResultReceiverRunner { + pub(crate) async fn run(mut self) { + let mut receiver = self.0.take().expect("Only take once"); + let stream = stream! { + while let Ok(changed) = receiver.recv().await { + yield changed; + } + }; + stream + .for_each(|changed| async { + match changed { + SearchResultChanged::SearchResultUpdate(notification) => { + send_notification( + SEARCH_ID, + SearchNotification::DidUpdateResults, + notification.channel.clone(), + ) + .payload(notification) + .send(); + }, + } + }) + .await; + } +} + +#[tracing::instrument(level = "trace")] +pub fn send_notification( + id: &str, + ty: SearchNotification, + channel: Option, +) -> NotificationBuilder { + let observable_source = &format!( + "{}{}", + SEARCH_OBSERVABLE_SOURCE, + channel.unwrap_or_default() + ); + + NotificationBuilder::new(id, ty, observable_source) +} diff --git a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs index 9c74850fcd..14a72c6ce6 100644 --- a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs +++ b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs @@ -60,7 +60,7 @@ impl AFCloudConfiguration { let enable_sync_trace = std::env::var(APPFLOWY_ENABLE_SYNC_TRACE) .map(|v| v == "true" || v == "1") - .unwrap_or(true); + .unwrap_or(false); Ok(Self { base_url, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index c8710470b0..9e67081eb7 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -12,15 +12,20 @@ crate-type = ["cdylib", "rlib"] tracing.workspace = true futures.workspace = true futures-util = "0.3.26" +reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } +hyper = "0.14" serde.workspace = true serde_json.workspace = true thiserror = "1.0" tokio = { workspace = true, features = ["sync"] } lazy_static = "1.4.0" bytes = { workspace = true, features = ["serde"] } +tokio-retry = "0.3" anyhow.workspace = true arc-swap.workspace = true +dashmap.workspace = true uuid.workspace = true +chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } collab = { workspace = true } collab-plugins = { workspace = true } collab-document = { workspace = true } @@ -28,6 +33,8 @@ collab-entity = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } collab-user = { workspace = true } +hex = "0.4.3" +postgrest = "1.0" lib-infra = { workspace = true } flowy-user-pub = { workspace = true } flowy-folder-pub = { workspace = true } @@ -39,13 +46,14 @@ flowy-search-pub = { workspace = true } flowy-storage = { workspace = true } flowy-storage-pub = { workspace = true } flowy-ai-pub = { workspace = true } +mime_guess = "2.0" +url = "2.4" tokio-util = "0.7" tokio-stream = { workspace = true, features = ["sync"] } +lib-dispatch = { workspace = true } +yrs.workspace = true rand = "0.8.5" semver = "1.0.23" -flowy-sqlite = { workspace = true } -flowy-ai = { workspace = true } -chrono.workspace = true [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 31114629ac..b0f09b1530 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,11 +1,4 @@ -use collab_plugins::CollabKVDB; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use uuid::Uuid; +use flowy_error::FlowyResult; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; pub const USER_UUID: &str = "uuid"; @@ -13,52 +6,7 @@ pub const USER_EMAIL: &str = "email"; pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. -#[async_trait] -pub trait LoggedUser: Send + Sync { +pub trait ServerUser: Send + Sync { /// different user might return different workspace id. - fn workspace_id(&self) -> FlowyResult; - - fn user_id(&self) -> FlowyResult; - async fn is_local_mode(&self) -> FlowyResult; - - fn get_sqlite_db(&self, uid: i64) -> Result; - - fn get_collab_db(&self, uid: i64) -> Result, FlowyError>; - - fn application_root_dir(&self) -> Result; -} - -// -pub struct AIUserServiceImpl(pub Weak); - -impl AIUserServiceImpl { - fn logged_user(&self) -> FlowyResult> { - self - .0 - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("User is not logged in")) - } -} - -#[async_trait] -impl AIUserService for AIUserServiceImpl { - fn user_id(&self) -> Result { - self.logged_user()?.user_id() - } - - async fn is_local_model(&self) -> FlowyResult { - self.logged_user()?.is_local_mode().await - } - - fn workspace_id(&self) -> Result { - self.logged_user()?.workspace_id() - } - - fn sqlite_connection(&self, uid: i64) -> Result { - self.logged_user()?.get_sqlite_db(uid) - } - - fn application_root_dir(&self) -> Result { - self.logged_user()?.application_root_dir() - } + fn workspace_id(&self) -> FlowyResult; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 6086f7084b..11fc5cc27c 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,4 +1,3 @@ -#![allow(unused_variables)] use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ ChatQuestionQuery, CompleteTextParams, RepeatedRelatedQuestion, ResponseFormat, @@ -8,41 +7,39 @@ use client_api::entity::chat_dto::{ RepeatedChatMessage, }; use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, ModelList, StreamAnswer, - StreamComplete, UpdateChatParams, + ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, LocalAIConfig, + ModelList, StreamAnswer, StreamComplete, SubscriptionPlan, UpdateChatParams, }; use flowy_error::FlowyError; use futures_util::{StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; +use lib_infra::util::{get_operating_system, OperatingSystem}; use serde_json::Value; use std::collections::HashMap; use std::path::Path; use tracing::trace; -use uuid::Uuid; -pub(crate) struct CloudChatServiceImpl { +pub(crate) struct AFCloudChatCloudServiceImpl { pub inner: T, } #[async_trait] -impl ChatCloudService for CloudChatServiceImpl +impl ChatCloudService for AFCloudChatCloudServiceImpl where T: AFServer, { async fn create_chat( &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - name: &str, - metadata: serde_json::Value, + _uid: &i64, + workspace_id: &str, + chat_id: &str, + rag_ids: Vec, ) -> Result<(), FlowyError> { let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatParams { chat_id, - name: name.to_string(), + name: "".to_string(), rag_ids, }; try_get_client? @@ -55,20 +52,23 @@ where async fn create_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, message_type: ChatMessageType, + metadata: &[ChatMessageMetadata], ) -> Result { + let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatMessageParams { content: message.to_string(), message_type, + metadata: metadata.to_vec(), }; let message = try_get_client? - .create_question(workspace_id, &chat_id, params) + .create_question(&workspace_id, &chat_id, params) .await .map_err(FlowyError::from)?; Ok(message) @@ -76,8 +76,8 @@ where async fn create_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, question_id: i64, metadata: Option, @@ -89,7 +89,7 @@ where question_message_id: question_id, }; let message = try_get_client? - .save_answer(workspace_id, chat_id.to_string().as_str(), params) + .save_answer(workspace_id, chat_id, params) .await .map_err(FlowyError::from)?; Ok(message) @@ -97,18 +97,16 @@ where async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, format: ResponseFormat, - ai_model: Option, ) -> Result { trace!( - "stream_answer: workspace_id={}, chat_id={}, format={:?}, model: {:?}", + "stream_answer: workspace_id={}, chat_id={}, format={:?}", workspace_id, chat_id, - format, - ai_model, + format ); let try_get_client = self.inner.try_get_client(); let result = try_get_client? @@ -119,7 +117,6 @@ where question_id: message_id, format, }, - ai_model.map(|v| v.name), ) .await; @@ -129,13 +126,13 @@ where async fn get_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_answer(workspace_id, chat_id.to_string().as_str(), question_id) + .get_answer(workspace_id, chat_id, question_message_id) .await .map_err(FlowyError::from)?; Ok(resp) @@ -143,14 +140,14 @@ where async fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, offset: MessageCursor, limit: u64, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_chat_messages(workspace_id, chat_id.to_string().as_str(), offset, limit) + .get_chat_messages(workspace_id, chat_id, offset, limit) .await .map_err(FlowyError::from)?; @@ -159,17 +156,13 @@ where async fn get_question_from_answer_id( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, answer_message_id: i64, ) -> Result { let try_get_client = self.inner.try_get_client()?; let resp = try_get_client - .get_question_message_from_answer_id( - workspace_id, - chat_id.to_string().as_str(), - answer_message_id, - ) + .get_question_message_from_answer_id(workspace_id, chat_id, answer_message_id) .await .map_err(FlowyError::from)? .ok_or_else(FlowyError::record_not_found)?; @@ -179,14 +172,13 @@ where async fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - ai_model: Option, ) -> Result { let try_get_client = self.inner.try_get_client(); let resp = try_get_client? - .get_chat_related_question(workspace_id, chat_id.to_string().as_str(), message_id) + .get_chat_related_question(workspace_id, chat_id, message_id) .await .map_err(FlowyError::from)?; @@ -195,62 +187,91 @@ where async fn stream_complete( &self, - workspace_id: &Uuid, + workspace_id: &str, params: CompleteTextParams, - ai_model: Option, ) -> Result { let stream = self .inner .try_get_client()? - .stream_completion_v2(workspace_id, params, ai_model.map(|v| v.name)) + .stream_completion_text(workspace_id, params) .await .map_err(FlowyError::from)? .map_err(FlowyError::from); - Ok(stream.boxed()) } - async fn embed_file( + async fn index_file( &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option>, + _workspace_id: &str, + _file_path: &Path, + _chat_id: &str, + _metadata: Option>, ) -> Result<(), FlowyError> { - Err( + return Err( FlowyError::not_support() .with_context("indexing file with appflowy cloud is not suppotred yet"), - ) + ); + } + + async fn get_local_ai_config(&self, workspace_id: &str) -> Result { + let system = get_operating_system(); + let platform = match system { + OperatingSystem::MacOS => "macos", + _ => { + return Err( + FlowyError::not_support() + .with_context("local ai is not supported on this operating system"), + ); + }, + }; + let config = self + .inner + .try_get_client()? + .get_local_ai_config(workspace_id, platform) + .await?; + Ok(config) + } + + async fn get_workspace_plan( + &self, + workspace_id: &str, + ) -> Result, FlowyError> { + let plans = self + .inner + .try_get_client()? + .get_active_workspace_subscriptions(workspace_id) + .await?; + Ok(plans) } async fn get_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, ) -> Result { let settings = self .inner .try_get_client()? - .get_chat_settings(workspace_id, chat_id.to_string().as_str()) + .get_chat_settings(workspace_id, chat_id) .await?; Ok(settings) } async fn update_chat_settings( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, params: UpdateChatParams, ) -> Result<(), FlowyError> { self .inner .try_get_client()? - .update_chat_settings(workspace_id, chat_id.to_string().as_str(), params) + .update_chat_settings(workspace_id, chat_id, params) .await?; Ok(()) } - async fn get_available_models(&self, workspace_id: &Uuid) -> Result { + async fn get_available_models(&self, workspace_id: &str) -> Result { let list = self .inner .try_get_client()? @@ -258,13 +279,4 @@ where .await?; Ok(list) } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result { - let setting = self - .inner - .try_get_client()? - .get_workspace_settings(workspace_id.to_string().as_str()) - .await?; - Ok(setting.ai_model) - } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index f29a7f89ad..4d264365ec 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,7 +1,3 @@ -#![allow(unused_variables)] -use crate::af_cloud::define::LoggedUser; -use crate::af_cloud::impls::util::check_request_workspace_id_is_match; -use crate::af_cloud::AFServer; use client_api::entity::ai_dto::{ SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams, }; @@ -10,20 +6,24 @@ use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; use collab::entity::EncodedCollab; use collab_entity::CollabType; +use serde_json::{Map, Value}; +use std::sync::Arc; +use tracing::{error, instrument}; + use flowy_database_pub::cloud::{ DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, TranslateRowContent, TranslateRowResponse, }; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use serde_json::{Map, Value}; -use std::sync::Weak; -use tracing::{error, instrument}; -use uuid::Uuid; + +use crate::af_cloud::define::ServerUser; +use crate::af_cloud::impls::util::check_request_workspace_id_is_match; +use crate::af_cloud::AFServer; pub(crate) struct AFCloudDatabaseCloudServiceImpl { pub inner: T, - pub logged_user: Weak, + pub user: Arc, } #[async_trait] @@ -35,21 +35,24 @@ where #[allow(clippy::blocks_in_conditions)] async fn get_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); let try_get_client = self.inner.try_get_client(); + let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, collab_type), + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id.clone(), collab_type.clone()), }; let result = try_get_client?.get_collab(params).await; match result { Ok(data) => { check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, + &workspace_id, + &cloned_user, format!("get database object: {}:{}", object_id, collab_type), )?; Ok(Some(data.encode_collab)) @@ -68,17 +71,17 @@ where #[allow(clippy::blocks_in_conditions)] async fn create_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, + workspace_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let encoded_collab_v1 = encoded_collab .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?; let params = CreateCollabParams { - workspace_id: *workspace_id, - object_id: *object_id, + workspace_id: workspace_id.to_string(), + object_id: object_id.to_string(), encoded_collab_v1, collab_type, }; @@ -89,22 +92,20 @@ where #[instrument(level = "debug", skip_all)] async fn batch_get_database_encode_collab( &self, - object_ids: Vec, + object_ids: Vec, object_ty: CollabType, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); + let cloned_user = self.user.clone(); let client = try_get_client?; let params = object_ids .into_iter() - .map(|object_id| QueryCollab::new(object_id, object_ty)) + .map(|object_id| QueryCollab::new(object_id, object_ty.clone())) .collect(); - let results = client.batch_get_collab(workspace_id, params).await?; - check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, - "batch get database object", - )?; + let results = client.batch_get_collab(&workspace_id, params).await?; + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "batch get database object")?; Ok( results .0 @@ -130,8 +131,8 @@ where async fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, - limit: usize, + _object_id: &str, + _limit: usize, ) -> Result, FlowyError> { Ok(vec![]) } @@ -144,17 +145,17 @@ where { async fn summary_database_row( &self, - workspace_id: &Uuid, - _object_id: &Uuid, - _summary_row: SummaryRowContent, + workspace_id: &str, + _object_id: &str, + summary_row: SummaryRowContent, ) -> Result { let try_get_client = self.inner.try_get_client(); - let map: Map = _summary_row + let map: Map = summary_row .into_iter() .map(|(key, value)| (key, Value::String(value))) .collect(); let params = SummarizeRowParams { - workspace_id: *workspace_id, + workspace_id: workspace_id.to_string(), data: SummarizeRowData::Content(map), }; let data = try_get_client?.summarize_row(params).await?; @@ -163,21 +164,19 @@ where async fn translate_database_row( &self, - workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, ) -> Result { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let data = TranslateRowData { - cells: _translate_row, - language: _language.to_string(), + cells: translate_row, + language: language.to_string(), include_header: false, }; - let params = TranslateRowParams { - workspace_id: workspace_id.to_string(), - data, - }; + let params = TranslateRowParams { workspace_id, data }; let data = try_get_client?.translate_row(params).await?; Ok(data) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 1e000d5971..d73bbe4c75 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -1,4 +1,3 @@ -#![allow(unused_variables)] use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -6,20 +5,20 @@ use collab::entity::EncodedCollab; use collab::preclude::Collab; use collab_document::document::Document; use collab_entity::CollabType; +use std::sync::Arc; +use tracing::instrument; + use flowy_document_pub::cloud::*; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use std::sync::Weak; -use tracing::instrument; -use uuid::Uuid; -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl { pub inner: T, - pub logged_user: Weak, + pub user: Arc, } #[async_trait] @@ -30,12 +29,12 @@ where #[instrument(level = "debug", skip_all, fields(document_id = %document_id))] async fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*document_id, CollabType::Document), + workspace_id: workspace_id.to_string(), + inner: QueryCollab::new(document_id.to_string(), CollabType::Document), }; let doc_state = self .inner @@ -49,7 +48,7 @@ where check_request_workspace_id_is_match( workspace_id, - &self.logged_user, + &self.user, format!("get document doc state:{}", document_id), )?; @@ -58,9 +57,9 @@ where async fn get_document_snapshots( &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, + _document_id: &str, + _limit: usize, + _workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } @@ -68,12 +67,12 @@ where #[instrument(level = "debug", skip_all)] async fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + workspace_id: &str, ) -> Result, FlowyError> { let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*document_id, CollabType::Document), + workspace_id: workspace_id.to_string(), + inner: QueryCollab::new(document_id.to_string(), CollabType::Document), }; let doc_state = self .inner @@ -85,12 +84,12 @@ where .to_vec(); check_request_workspace_id_is_match( workspace_id, - &self.logged_user, + &self.user, format!("Get {} document", document_id), )?; let collab = Collab::new_with_source( CollabOrigin::Empty, - document_id.to_string().as_str(), + document_id, DataSource::DocStateV1(doc_state), vec![], false, @@ -101,13 +100,13 @@ where async fn create_document_collab( &self, - workspace_id: &Uuid, - document_id: &Uuid, + workspace_id: &str, + document_id: &str, encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { let params = CreateCollabParams { - workspace_id: *workspace_id, - object_id: *document_id, + workspace_id: workspace_id.to_string(), + object_id: document_id.to_string(), encoded_collab_v1: encoded_collab .encode_to_bytes() .map_err(|err| FlowyError::internal().with_context(err))?, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs index 8db806a0da..9f4e15f430 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs @@ -1,10 +1,9 @@ use crate::af_cloud::AFServer; use client_api::entity::{CompleteUploadRequest, CreateUploadRequest}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError}; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub struct AFCloudFileStorageServiceImpl { pub client: T, @@ -57,10 +56,10 @@ where async fn get_object_url_v1( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, - ) -> FlowyResult { + ) -> Result { let url = self .client .try_get_client()? @@ -68,14 +67,14 @@ where Ok(url) } - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { + async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)> { let value = self.client.try_get_client().ok()?.parse_blob_url_v1(url)?; Some(value) } async fn create_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, content_type: &str, @@ -110,7 +109,7 @@ where async fn upload_part( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -135,7 +134,7 @@ where async fn complete_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 578f2870c6..5457164a87 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,29 +1,34 @@ use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::{ - CollabParams, PublishCollabItem, PublishCollabMetadata, QueryCollab, QueryCollabParams, + workspace_dto::CreateWorkspaceParam, CollabParams, PublishCollabItem, PublishCollabMetadata, + QueryCollab, QueryCollabParams, }; use client_api::entity::{PatchPublishedCollab, PublishInfo}; +use collab::core::collab::DataSource; +use collab::core::origin::CollabOrigin; use collab_entity::CollabType; +use collab_folder::RepeatedViewIdentifier; use serde_json::to_vec; use std::path::PathBuf; -use std::sync::Weak; +use std::sync::Arc; use tracing::{instrument, trace}; use uuid::Uuid; -use flowy_error::FlowyError; +use flowy_error::{ErrorCode, FlowyError}; use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, + Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, FullSyncCollabParams, + Workspace, WorkspaceRecord, }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl { pub inner: T, - pub logged_user: Weak, + pub user: Arc, } #[async_trait] @@ -31,6 +36,87 @@ impl FolderCloudService for AFCloudFolderCloudServiceImpl where T: AFServer, { + async fn create_workspace(&self, _uid: i64, name: &str) -> Result { + let try_get_client = self.inner.try_get_client(); + let cloned_name = name.to_string(); + + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(cloned_name), + }) + .await?; + + Ok(Workspace { + id: new_workspace.workspace_id.to_string(), + name: new_workspace.workspace_name, + created_at: new_workspace.created_at.timestamp(), + child_views: RepeatedViewIdentifier::new(vec![]), + created_by: Some(new_workspace.owner_uid), + last_edited_time: new_workspace.created_at.timestamp(), + last_edited_by: Some(new_workspace.owner_uid), + }) + } + + async fn open_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + + let client = try_get_client?; + let _ = client.open_workspace(&workspace_id).await?; + Ok(()) + } + + async fn get_all_workspace(&self) -> Result, FlowyError> { + let try_get_client = self.inner.try_get_client(); + + let client = try_get_client?; + let records = client + .get_user_workspace_info() + .await? + .workspaces + .into_iter() + .map(|af_workspace| WorkspaceRecord { + id: af_workspace.workspace_id.to_string(), + name: af_workspace.workspace_name, + created_at: af_workspace.created_at.timestamp(), + }) + .collect::>(); + Ok(records) + } + + #[instrument(level = "debug", skip_all)] + async fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> Result, FlowyError> { + let uid = *uid; + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + let cloned_user = self.user.clone(); + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder data")?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Empty, + DataSource::DocStateV1(doc_state), + &workspace_id, + vec![], + )?; + Ok(folder.get_folder_data(&workspace_id)) + } + async fn get_folder_snapshots( &self, _workspace_id: &str, @@ -42,15 +128,18 @@ where #[instrument(level = "debug", skip_all)] async fn get_folder_doc_state( &self, - workspace_id: &Uuid, + workspace_id: &str, _uid: i64, collab_type: CollabType, - object_id: &Uuid, + object_id: &str, ) -> Result, FlowyError> { + let object_id = object_id.to_string(); + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); + let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, collab_type), + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, collab_type), }; let doc_state = try_get_client? .get_collab(params) @@ -59,19 +148,20 @@ where .encode_collab .doc_state .to_vec(); - check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder doc state")?; + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder doc state")?; Ok(doc_state) } async fn full_sync_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, params: FullSyncCollabParams, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); try_get_client? .collab_full_sync( - workspace_id, + &workspace_id, ¶ms.object_id, params.collab_type, params.encoded_collab.doc_state.to_vec(), @@ -83,9 +173,10 @@ where async fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = objects .into_iter() @@ -98,7 +189,7 @@ where }) .collect::>(); try_get_client? - .create_collab_list(workspace_id, params) + .create_collab_list(&workspace_id, params) .await?; Ok(()) } @@ -109,9 +200,10 @@ where async fn publish_view( &self, - workspace_id: &Uuid, + workspace_id: &str, payload: Vec, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = payload .into_iter() @@ -136,27 +228,36 @@ where }) .collect::>(); try_get_client? - .publish_collabs(workspace_id, params) + .publish_collabs(&workspace_id, params) .await?; Ok(()) } async fn unpublish_views( &self, - workspace_id: &Uuid, - view_ids: Vec, + workspace_id: &str, + view_ids: Vec, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); + let view_uuids = view_ids + .iter() + .map(|id| Uuid::parse_str(id).unwrap_or(Uuid::nil())) + .collect::>(); try_get_client? - .unpublish_collabs(workspace_id, &view_ids) + .unpublish_collabs(&workspace_id, &view_uuids) .await?; Ok(()) } - async fn get_publish_info(&self, view_id: &Uuid) -> Result { + async fn get_publish_info(&self, view_id: &str) -> Result { let try_get_client = self.inner.try_get_client(); + let view_id = Uuid::parse_str(view_id) + .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id")); + + let view_id = view_id?; let info = try_get_client? - .get_published_collab_info(view_id) + .get_published_collab_info(&view_id) .await .map_err(FlowyError::from)?; Ok(info) @@ -164,11 +265,14 @@ where async fn set_publish_name( &self, - workspace_id: &Uuid, - view_id: Uuid, + workspace_id: &str, + view_id: String, new_name: String, ) -> Result<(), FlowyError> { let try_get_client = self.inner.try_get_client()?; + let view_id = Uuid::parse_str(&view_id) + .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id"))?; + try_get_client .patch_published_collabs( workspace_id, @@ -186,24 +290,36 @@ where async fn set_publish_namespace( &self, - workspace_id: &Uuid, + workspace_id: &str, new_namespace: String, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); try_get_client? - .set_workspace_publish_namespace(workspace_id, new_namespace) + .set_workspace_publish_namespace(&workspace_id, new_namespace) .await?; Ok(()) } + async fn get_publish_namespace(&self, workspace_id: &str) -> Result { + let workspace_id = workspace_id.to_string(); + let namespace = self + .inner + .try_get_client()? + .get_workspace_publish_namespace(&workspace_id) + .await?; + Ok(namespace) + } + async fn list_published_views( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result, FlowyError> { + let workspace_id = workspace_id.to_string(); let published_views = self .inner .try_get_client()? - .list_published_views(workspace_id) + .list_published_views(&workspace_id) .await .map_err(FlowyError::from)?; Ok(published_views) @@ -211,7 +327,7 @@ where async fn get_default_published_view_info( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result { let default_published_view_info = self .inner @@ -224,7 +340,7 @@ where async fn set_default_published_view( &self, - workspace_id: &Uuid, + workspace_id: &str, view_id: uuid::Uuid, ) -> Result<(), FlowyError> { self @@ -236,7 +352,7 @@ where Ok(()) } - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + async fn remove_default_published_view(&self, workspace_id: &str) -> Result<(), FlowyError> { self .inner .try_get_client()? @@ -246,15 +362,6 @@ where Ok(()) } - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { - let namespace = self - .inner - .try_get_client()? - .get_workspace_publish_namespace(workspace_id) - .await?; - Ok(namespace) - } - async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { let file_path = PathBuf::from(file_path); let client = self.inner.try_get_client()?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs index 1ce0995144..552a94068a 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs @@ -1,16 +1,18 @@ -use crate::af_cloud::AFServer; -use flowy_ai_pub::cloud::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; +use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use flowy_search_pub::cloud::SearchCloudService; use lib_infra::async_trait::async_trait; -use uuid::Uuid; + +use crate::af_cloud::AFServer; pub(crate) struct AFCloudSearchCloudServiceImpl { pub inner: T, } +// The limit of what the score should be for results, used to +// filter out irrelevant results. +// https://community.openai.com/t/rule-of-thumb-cosine-similarity-thresholds/693670/5 +const SCORE_LIMIT: f64 = 0.3; const DEFAULT_PREVIEW: u32 = 80; #[async_trait] @@ -20,27 +22,19 @@ where { async fn document_search( &self, - workspace_id: &Uuid, + workspace_id: &str, query: String, ) -> Result, FlowyError> { let client = self.inner.try_get_client()?; let result = client - .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW, None) + .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW) .await?; - Ok(result) - } - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec, - ) -> Result { - let client = self.inner.try_get_client()?; - let result = client - .generate_search_summary(workspace_id, &query, search_results) - .await?; + // Filter out irrelevant results + let result = result + .into_iter() + .filter(|r| r.score > SCORE_LIMIT) + .collect(); Ok(result) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 4e46f310c5..208281fc5f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use std::str::FromStr; -use std::sync::{Arc, Weak}; +use std::sync::Arc; use anyhow::anyhow; use arc_swap::ArcSwapOption; @@ -13,50 +12,50 @@ use client_api::entity::workspace_dto::{ WorkspaceMemberInvitation, }; use client_api::entity::{ - AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, AuthProvider, - CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, + AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, + AuthProvider, CollabParams, CreateCollabParams, QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; use collab_entity::{CollabObject, CollabType}; use tracing::{instrument, trace}; -use crate::af_cloud::define::{LoggedUser, USER_SIGN_IN_URL}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; +use flowy_user_pub::entities::{ + AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserCredentials, UserProfile, + UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +}; +use lib_infra::async_trait::async_trait; +use lib_infra::box_any::BoxAny; +use uuid::Uuid; + +use crate::af_cloud::define::{ServerUser, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; use crate::af_cloud::impls::user::util::encryption_type_from_profile; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::{AFCloudClient, AFServer}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; -use flowy_user_pub::entities::{ - AFCloudOAuthParams, AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, -}; -use flowy_user_pub::sql::select_user_workspace; -use lib_infra::async_trait::async_trait; -use lib_infra::box_any::BoxAny; -use uuid::Uuid; use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_status}; pub(crate) struct AFCloudUserAuthServiceImpl { server: T, user_change_recv: ArcSwapOption>, - logged_user: Weak, + user: Arc, } impl AFCloudUserAuthServiceImpl { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver, - logged_user: Weak, + user: Arc, ) -> Self { Self { server, user_change_recv: ArcSwapOption::new(Some(Arc::new(user_change_recv))), - logged_user, + user, } } } @@ -121,13 +120,16 @@ where &self, email: &str, password: &str, - ) -> Result { + ) -> Result { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let response = client.sign_in_password(&email, &password).await?; - Ok(response.gotrue_response) + client.sign_in_password(&email, &password).await?; + let profile = client.get_profile().await?; + let token = client.get_token()?; + let profile = user_profile_from_af_profile(token, profile)?; + Ok(profile) } async fn sign_in_with_magic_link( @@ -145,19 +147,6 @@ where Ok(()) } - async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result { - let email = email.to_owned(); - let passcode = passcode.to_owned(); - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let response = client.sign_in_with_passcode(&email, &passcode).await?; - Ok(response) - } - async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result { let provider = AuthProvider::from(provider); let try_get_client = self.server.try_get_client(); @@ -168,7 +157,11 @@ where Ok(url) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + async fn update_user( + &self, + _credential: UserCredentials, + params: UpdateUserProfileParams, + ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; client @@ -180,35 +173,27 @@ where #[instrument(level = "debug", skip_all)] async fn get_user_profile( &self, - uid: i64, - workspace_id: &str, + _credential: UserCredentials, ) -> Result { - let client = self.server.try_get_client()?; - let logged_user = self - .logged_user - .upgrade() - .ok_or_else(FlowyError::user_not_login)?; - + let try_get_client = self.server.try_get_client(); + let cloned_user = self.user.clone(); + let expected_workspace_id = cloned_user.workspace_id()?; + let client = try_get_client?; let profile = client.get_profile().await?; let token = client.get_token()?; - - let mut conn = logged_user.get_sqlite_db(uid)?; - let workspace_auth_type = select_user_workspace(workspace_id, &mut conn) - .map(|row| AuthType::from(row.workspace_type)) - .unwrap_or(AuthType::AppFlowyCloud); - let profile = user_profile_from_af_profile(token, profile, workspace_auth_type)?; + let profile = user_profile_from_af_profile(token, profile)?; // Discard the response if the user has switched to a new workspace. This avoids updating the // user profile with potentially outdated information when the workspace ID no longer matches. - let workspace_id = Uuid::from_str(workspace_id)?; - check_request_workspace_id_is_match(&workspace_id, &self.logged_user, "get user profile")?; + check_request_workspace_id_is_match(&expected_workspace_id, &cloned_user, "get user profile")?; Ok(profile) } - async fn open_workspace(&self, workspace_id: &Uuid) -> Result { + async fn open_workspace(&self, workspace_id: &str) -> Result { let try_get_client = self.server.try_get_client(); + let workspace_id = workspace_id.to_string(); let client = try_get_client?; - let af_workspace = client.open_workspace(workspace_id).await?; + let af_workspace = client.open_workspace(&workspace_id).await?; Ok(to_user_workspace(af_workspace)) } @@ -224,10 +209,10 @@ where } async fn create_workspace(&self, workspace_name: &str) -> Result { + let try_get_client = self.server.try_get_client(); let workspace_name_owned = workspace_name.to_owned(); - let new_workspace = self - .server - .try_get_client()? + let client = try_get_client?; + let new_workspace = client .create_workspace(CreateWorkspaceParam { workspace_name: Some(workspace_name_owned), }) @@ -237,34 +222,40 @@ where async fn patch_workspace( &self, - workspace_id: &Uuid, - new_workspace_name: Option, - new_workspace_icon: Option, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_owned(); - self - .server - .try_get_client()? + let try_get_client = self.server.try_get_client(); + let owned_workspace_id = workspace_id.to_owned(); + let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); + let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); + let workspace_id: Uuid = owned_workspace_id + .parse() + .map_err(|_| ErrorCode::InvalidParams)?; + let client = try_get_client?; + client .patch_workspace(PatchWorkspaceParam { workspace_id, - workspace_name: new_workspace_name, - workspace_icon: new_workspace_icon, + workspace_name: owned_workspace_name, + workspace_icon: owned_workspace_icon, }) .await?; Ok(()) } - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); + let workspace_id_owned = workspace_id.to_owned(); let client = try_get_client?; - client.delete_workspace(workspace_id).await?; + client.delete_workspace(&workspace_id_owned).await?; Ok(()) } async fn invite_workspace_member( &self, invitee_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); @@ -309,11 +300,11 @@ where async fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); try_get_client? - .remove_workspace_members(&workspace_id, vec![user_email]) + .remove_workspace_members(workspace_id, vec![user_email]) .await?; Ok(()) } @@ -321,20 +312,20 @@ where async fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); try_get_client? - .update_workspace_member(&workspace_id, changeset) + .update_workspace_member(workspace_id, changeset) .await?; Ok(()) } async fn get_workspace_members( &self, - workspace_id: Uuid, + workspace_id: String, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let members = try_get_client? @@ -346,21 +337,38 @@ where Ok(members) } + async fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> Result { + let try_get_client = self.server.try_get_client(); + let client = try_get_client?; + let query = QueryWorkspaceMember { + workspace_id: workspace_id.clone(), + uid, + }; + let member = client.get_workspace_member(query).await?; + Ok(from_af_workspace_member(member)) + } + #[instrument(level = "debug", skip_all)] async fn get_user_awareness_doc_state( &self, _uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, + workspace_id: &str, + object_id: &str, ) -> Result, FlowyError> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); let try_get_client = self.server.try_get_client(); - let cloned_user = self.logged_user.clone(); + let cloned_user = self.user.clone(); let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, CollabType::UserAwareness), + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, CollabType::UserAwareness), }; let resp = try_get_client?.get_collab(params).await?; - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get user awareness object")?; + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get user awareness object")?; Ok(resp.encode_collab.doc_state.to_vec()) } @@ -369,6 +377,10 @@ where Arc::into_inner(rx) } + async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { + Ok(()) + } + async fn create_collab_object( &self, collab_object: &CollabObject, @@ -377,12 +389,9 @@ where let try_get_client = self.server.try_get_client(); let collab_object = collab_object.clone(); let client = try_get_client?; - let workspace_id = Uuid::from_str(&collab_object.workspace_id)?; - let object_id = Uuid::from_str(&collab_object.object_id)?; - let params = CreateCollabParams { - workspace_id, - object_id, + workspace_id: collab_object.workspace_id, + object_id: collab_object.object_id, collab_type: collab_object.collab_type, encoded_collab_v1: data, }; @@ -392,43 +401,41 @@ where async fn batch_create_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec, ) -> Result<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); let params = objects .into_iter() - .flat_map(|object| { - Uuid::from_str(&object.object_id) - .map(|object_id| { - CollabParams::new( - object_id, - u8::from(object.collab_type).into(), - object.encoded_collab, - ) - }) - .ok() + .map(|object| { + CollabParams::new( + object.object_id, + u8::from(object.collab_type).into(), + object.encoded_collab, + ) }) .collect::>(); try_get_client? - .create_collab_list(workspace_id, params) + .create_collab_list(&workspace_id, params) .await .map_err(FlowyError::from)?; Ok(()) } - async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { let try_get_client = self.server.try_get_client(); + let workspace_id = workspace_id.to_string(); let client = try_get_client?; - client.leave_workspace(workspace_id).await?; + client.leave_workspace(&workspace_id).await?; Ok(()) } async fn subscribe_workspace( &self, - workspace_id: Uuid, + workspace_id: String, recurring_interval: RecurringInterval, - workspace_subscription_plan: SubscriptionPlan, + subscription_plan: SubscriptionPlan, success_url: String, ) -> Result { let try_get_client = self.server.try_get_client(); @@ -438,27 +445,37 @@ where .create_subscription( &workspace_id, recurring_interval, - workspace_subscription_plan, + subscription_plan, &success_url, ) .await?; Ok(payment_link) } - async fn get_workspace_member( + async fn get_workspace_member_info( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, ) -> Result { let try_get_client = self.server.try_get_client(); + let workspace_id = workspace_id.to_string(); let client = try_get_client?; let params = QueryWorkspaceMember { - workspace_id: *workspace_id, + workspace_id: workspace_id.to_string(), uid, }; let member = client.get_workspace_member(params).await?; - - Ok(from_af_workspace_member(member)) + let role = match member.role { + AFRole::Owner => Role::Owner, + AFRole::Member => Role::Member, + AFRole::Guest => Role::Guest, + }; + Ok(WorkspaceMember { + email: member.email, + role, + name: member.name, + avatar_url: member.avatar_url, + }) } async fn get_workspace_subscriptions( @@ -472,13 +489,11 @@ where async fn get_workspace_subscription_one( &self, - workspace_id: &Uuid, + workspace_id: String, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let workspace_subscriptions = client - .get_workspace_subscriptions(&workspace_id.to_string()) - .await?; + let workspace_subscriptions = client.get_workspace_subscriptions(&workspace_id).await?; Ok(workspace_subscriptions) } @@ -503,25 +518,23 @@ where async fn get_workspace_plan( &self, - workspace_id: Uuid, + workspace_id: String, ) -> Result, FlowyError> { let try_get_client = self.server.try_get_client(); let client = try_get_client?; let plans = client - .get_active_workspace_subscriptions(&workspace_id.to_string()) + .get_active_workspace_subscriptions(&workspace_id) .await?; Ok(plans) } async fn get_workspace_usage( &self, - workspace_id: &Uuid, + workspace_id: String, ) -> Result { let try_get_client = self.server.try_get_client(); let client = try_get_client?; - let usage = client - .get_workspace_usage_and_limit(&workspace_id.to_string()) - .await?; + let usage = client.get_workspace_usage_and_limit(&workspace_id).await?; Ok(usage) } @@ -534,7 +547,7 @@ where async fn update_workspace_subscription_payment_period( &self, - workspace_id: &Uuid, + workspace_id: String, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { @@ -542,7 +555,7 @@ where let client = try_get_client?; client .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { - workspace_id: workspace_id.to_string(), + workspace_id, plan, recurring_interval, }) @@ -559,7 +572,7 @@ where async fn get_workspace_setting( &self, - workspace_id: &Uuid, + workspace_id: &str, ) -> Result { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); @@ -570,7 +583,7 @@ where async fn update_workspace_setting( &self, - workspace_id: &Uuid, + workspace_id: &str, workspace_settings: AFWorkspaceSettingsChange, ) -> Result { trace!("Sync workspace settings: {:?}", workspace_settings); @@ -656,7 +669,6 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace { icon: af_workspace.icon, member_count: af_workspace.member_count.unwrap_or(0), role: af_workspace.role.map(|r| r.into()), - workspace_type: AuthType::AppFlowyCloud, } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index 838e9dd6ca..0710bcc2b2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -3,12 +3,22 @@ use client_api::entity::auth_dto::{UpdateUserParams, UserMetaData}; use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFWorkspaceMember}; use flowy_user_pub::entities::{ - AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, - USER_METADATA_ICON_URL, + Authenticator, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, + WorkspaceMember, USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, + USER_METADATA_STABILITY_AI_KEY, }; +use crate::af_cloud::impls::user::util::encryption_type_from_profile; + pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUserParams { let mut user_metadata = UserMetaData::new(); + if let Some(openai_key) = update.openai_key { + user_metadata.insert(USER_METADATA_OPEN_AI_KEY, openai_key); + } + + if let Some(stability_ai_key) = update.stability_ai_key { + user_metadata.insert(USER_METADATA_STABILITY_AI_KEY, stability_ai_key); + } if let Some(icon_url) = update.icon_url { user_metadata.insert(USER_METADATA_ICON_URL, icon_url); @@ -25,14 +35,20 @@ pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUs pub fn user_profile_from_af_profile( token: String, profile: AFUserProfile, - workspace_auth_type: AuthType, ) -> Result { - let icon_url = { + let encryption_type = encryption_type_from_profile(&profile); + let (icon_url, openai_key, stability_ai_key) = { profile .metadata .map(|m| { - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + ( + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + m.get(USER_METADATA_OPEN_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + m.get(USER_METADATA_STABILITY_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + ) }) .unwrap_or_default() }; @@ -42,10 +58,13 @@ pub fn user_profile_from_af_profile( name: profile.name.unwrap_or("".to_string()), token, icon_url: icon_url.unwrap_or_default(), - auth_type: AuthType::AppFlowyCloud, + openai_key: openai_key.unwrap_or_default(), + stability_ai_key: stability_ai_key.unwrap_or_default(), + authenticator: Authenticator::AppFlowyCloud, + encryption_type, uid: profile.uid, updated_at: profile.updated_at, - workspace_auth_type, + ai_model: "".to_string(), }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index 300738c833..4075a5b908 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,24 +1,22 @@ -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use flowy_error::{FlowyError, FlowyResult}; -use std::sync::Weak; +use std::sync::Arc; use tracing::warn; -use uuid::Uuid; /// Validates the workspace_id provided in the request. /// It checks that the workspace_id from the request matches the current user's active workspace_id. /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( - expected_workspace_id: &Uuid, - user: &Weak, + expected_workspace_id: &str, + user: &Arc, action: impl AsRef, ) -> FlowyResult<()> { - let user = user.upgrade().ok_or_else(FlowyError::user_not_login)?; let actual_workspace_id = user.workspace_id()?; - if expected_workspace_id != &actual_workspace_id { + if expected_workspace_id != actual_workspace_id { warn!( "{}, expect workspace_id: {}, actual workspace_id: {}", action.as_ref(), - expected_workspace_id.to_string(), + expected_workspace_id, actual_workspace_id ); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 500c78c930..06e56a8c05 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,8 +1,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; +use std::sync::Arc; use std::time::Duration; -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use anyhow::Error; use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; @@ -24,13 +24,6 @@ use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; use flowy_user_pub::entities::UserTokenState; -use crate::af_cloud::impls::{ - AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, - AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl, -}; -use crate::AppFlowyServer; -use flowy_ai::offline::offline_message_sync::AutoSyncChatService; -use flowy_ai_pub::user_service::AIUserService; use rand::Rng; use semver::Version; use tokio::select; @@ -41,6 +34,13 @@ use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use uuid::Uuid; +use crate::af_cloud::impls::{ + AFCloudChatCloudServiceImpl, AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, + AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, +}; + +use crate::AppFlowyServer; + use super::impls::AFCloudSearchCloudServiceImpl; pub(crate) type AFCloudClient = Client; @@ -53,8 +53,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc, pub device_id: String, ws_client: Arc, - logged_user: Weak, - ai_user_service: Arc, + user: Arc, } impl AppFlowyCloudServer { @@ -63,8 +62,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - logged_user: Weak, - ai_user_service: Arc, + user: Arc, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -93,8 +91,8 @@ impl AppFlowyCloudServer { ); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); - spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); + spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); Self { config, client: api_client, @@ -102,18 +100,16 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - logged_user, - ai_user_service, + user, } } - fn get_server_impl(&self) -> AFServerImpl { - let client = if self.enable_sync.load(Ordering::SeqCst) { + fn get_client(&self) -> Option> { + if self.enable_sync.load(Ordering::SeqCst) { Some(self.client.clone()) } else { None - }; - AFServerImpl { client } + } } } @@ -169,6 +165,9 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn user_service(&self) -> Arc { + let server = AFServerImpl { + client: self.get_client(), + }; let mut user_change = self.ws_client.subscribe_user_changed(); let (tx, rx) = tokio::sync::mpsc::channel(1); tokio::spawn(async move { @@ -186,47 +185,57 @@ impl AppFlowyServer for AppFlowyCloudServer { }); Arc::new(AFCloudUserAuthServiceImpl::new( - self.get_server_impl(), + server, rx, - self.logged_user.clone(), + self.user.clone(), )) } fn folder_service(&self) -> Arc { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudFolderCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } fn database_service(&self) -> Arc { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } fn database_ai_service(&self) -> Option> { + let server = AFServerImpl { + client: self.get_client(), + }; Some(Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), })) } fn document_service(&self) -> Arc { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudDocumentCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } fn chat_service(&self) -> Arc { - Arc::new(AutoSyncChatService::new( - Arc::new(CloudChatServiceImpl { - inner: self.get_server_impl(), - }), - self.ai_user_service.clone(), - )) + let server = AFServerImpl { + client: self.get_client(), + }; + Arc::new(AFCloudChatCloudServiceImpl { inner: server }) } fn subscribe_ws_state(&self) -> Option { @@ -256,16 +265,21 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn file_storage(&self) -> Option> { + let client = AFServerImpl { + client: self.get_client(), + }; Some(Arc::new(AFCloudFileStorageServiceImpl::new( - self.get_server_impl(), + client, self.config.maximum_upload_file_size_in_bytes, ))) } fn search_service(&self) -> Option> { - Some(Arc::new(AFCloudSearchCloudServiceImpl { - inner: self.get_server_impl(), - })) + let server = AFServerImpl { + client: self.get_client(), + }; + + Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server })) } } diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs new file mode 100644 index 0000000000..194aa89ef3 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -0,0 +1,151 @@ +use client_api::entity::ai_dto::{LocalAIConfig, RepeatedRelatedQuestion}; +use flowy_ai_pub::cloud::{ + ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, ChatSettings, + CompleteTextParams, MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, + StreamComplete, SubscriptionPlan, UpdateChatParams, +}; +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; +use serde_json::Value; +use std::collections::HashMap; +use std::path::Path; + +pub(crate) struct DefaultChatCloudServiceImpl; + +#[async_trait] +impl ChatCloudService for DefaultChatCloudServiceImpl { + async fn create_chat( + &self, + _uid: &i64, + _workspace_id: &str, + _chat_id: &str, + _rag_ids: Vec, + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn create_question( + &self, + _workspace_id: &str, + _chat_id: &str, + _message: &str, + _message_type: ChatMessageType, + _metadata: &[ChatMessageMetadata], + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn create_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _message: &str, + _question_id: i64, + _metadata: Option, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn stream_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _message_id: i64, + _format: ResponseFormat, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn get_chat_messages( + &self, + _workspace_id: &str, + _chat_id: &str, + _offset: MessageCursor, + _limit: u64, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn get_question_from_answer_id( + &self, + _workspace_id: &str, + _chat_id: &str, + _answer_id: i64, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn get_related_message( + &self, + _workspace_id: &str, + _chat_id: &str, + _message_id: i64, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn get_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _question_message_id: i64, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn stream_complete( + &self, + _workspace_id: &str, + _params: CompleteTextParams, + ) -> Result { + Err(FlowyError::not_support().with_context("complete text is not supported in local server.")) + } + + async fn index_file( + &self, + _workspace_id: &str, + _file_path: &Path, + _chat_id: &str, + _metadata: Option>, + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support().with_context("indexing file is not supported in local server.")) + } + + async fn get_local_ai_config(&self, _workspace_id: &str) -> Result { + Err( + FlowyError::not_support() + .with_context("Get local ai config is not supported in local server."), + ) + } + + async fn get_workspace_plan( + &self, + _workspace_id: &str, + ) -> Result, FlowyError> { + Err( + FlowyError::not_support() + .with_context("Get local ai config is not supported in local server."), + ) + } + + async fn get_chat_settings( + &self, + _workspace_id: &str, + _chat_id: &str, + ) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn update_chat_settings( + &self, + _workspace_id: &str, + _chat_id: &str, + _params: UpdateChatParams, + ) -> Result<(), FlowyError> { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + async fn get_available_models(&self, _workspace_id: &str) -> Result { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } +} diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 034991a984..33f4b0c0d8 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -5,4 +5,5 @@ pub mod local_server; mod response; mod server; +mod default_impl; pub mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs deleted file mode 100644 index 845b6dec1c..0000000000 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::af_cloud::define::LoggedUser; -use chrono::{TimeZone, Utc}; -use client_api::entity::ai_dto::RepeatedRelatedQuestion; -use client_api::entity::CompletionStream; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_ai::local_ai::stream_util::QuestionStream; -use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; -use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, - ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, - ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, DEFAULT_AI_MODEL_NAME, -}; -use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, - select_answer_where_match_reply_message_id, select_chat_messages, select_message_content, - serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, upsert_chat_messages, - ChatMessageTable, ChatTable, ChatTableChangeset, -}; -use flowy_error::{FlowyError, FlowyResult}; -use futures_util::{stream, StreamExt, TryStreamExt}; -use lib_infra::async_trait::async_trait; -use lib_infra::util::timestamp; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::path::Path; -use std::sync::Arc; -use tracing::trace; -use uuid::Uuid; - -pub struct LocalChatServiceImpl { - pub logged_user: Arc, - pub local_ai: Arc, -} - -impl LocalChatServiceImpl { - fn get_message_content(&self, message_id: i64) -> FlowyResult { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let content = select_message_content(db, message_id)?.ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) - })?; - Ok(content) - } - - async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let conn = self.logged_user.get_sqlite_db(uid)?; - let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); - upsert_chat_messages(conn, &[row])?; - Ok(()) - } -} - -#[async_trait] -impl ChatCloudService for LocalChatServiceImpl { - async fn create_chat( - &self, - _uid: &i64, - _workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec, - _name: &str, - metadata: Value, - ) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); - upsert_chat(db, &row)?; - Ok(()) - } - - async fn create_question( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result { - let message = match message_type { - ChatMessageType::System => ChatMessage::new_system(timestamp(), message.to_string()), - ChatMessageType::User => ChatMessage::new_human(timestamp(), message.to_string(), None), - }; - - self.upsert_message(chat_id, message.clone()).await?; - Ok(message) - } - - async fn create_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option, - ) -> Result { - let mut message = ChatMessage::new_ai(timestamp(), message.to_string(), Some(question_id)); - if let Some(metadata) = metadata { - message.metadata = metadata; - } - self.upsert_message(chat_id, message.clone()).await?; - Ok(message) - } - - async fn stream_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - format: ResponseFormat, - _ai_model: Option, - ) -> Result { - if self.local_ai.is_running() { - let content = self.get_message_content(message_id)?; - match self - .local_ai - .stream_question( - &chat_id.to_string(), - &content, - Some(json!(format)), - json!({}), - ) - .await - { - Ok(stream) => Ok(QuestionStream::new(stream).boxed()), - Err(err) => Ok( - stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), - ), - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } - - async fn get_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - - match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { - None => Err(FlowyError::record_not_found()), - Some(message) => Ok(chat_message_from_row(message)), - } - } - - async fn get_chat_messages( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let result = select_chat_messages(db, &chat_id, limit, offset)?; - - let messages = result - .messages - .into_iter() - .map(chat_message_from_row) - .collect(); - - Ok(RepeatedChatMessage { - messages, - has_more: result.has_more, - total: result.total_count, - }) - } - - async fn get_question_from_answer_id( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? - .map(chat_message_from_row) - .ok_or_else(FlowyError::record_not_found)?; - Ok(row) - } - - async fn get_related_message( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - _ai_model: Option, - ) -> Result { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); - - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, - }) - .collect::>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) - } else { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], - }) - } - } - - async fn stream_complete( - &self, - _workspace_id: &Uuid, - params: CompleteTextParams, - _ai_model: Option, - ) -> Result { - if self.local_ai.is_running() { - match self - .local_ai - .complete_text_v2( - ¶ms.text, - params.completion_type.unwrap() as u8, - Some(json!(params.format)), - Some(json!(params.metadata)), - ) - .await - { - Ok(stream) => Ok( - CompletionStream::new( - stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), - ) - .map_err(FlowyError::from) - .boxed(), - ), - Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } - - async fn embed_file( - &self, - _workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option>, - ) -> Result<(), FlowyError> { - if self.local_ai.is_running() { - self - .local_ai - .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - Ok(()) - } else { - Err(FlowyError::local_ai_not_ready()) - } - } - - async fn get_chat_settings( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = read_chat(db, &chat_id)?; - let rag_ids = deserialize_rag_ids(&row.rag_ids); - let metadata = deserialize_chat_metadata::(&row.metadata); - let setting = ChatSettings { - name: row.name, - rag_ids, - metadata, - }; - - Ok(setting) - } - - async fn update_chat_settings( - &self, - _workspace_id: &Uuid, - id: &Uuid, - s: UpdateChatParams, - ) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let mut db = self.logged_user.get_sqlite_db(uid)?; - let changeset = ChatTableChangeset { - chat_id: id.to_string(), - name: s.name, - metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), - rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), - is_sync: None, - }; - - update_chat(&mut db, changeset)?; - Ok(()) - } - - async fn get_available_models(&self, _workspace_id: &Uuid) -> Result { - Ok(ModelList { models: vec![] }) - } - - async fn get_workspace_default_model(&self, _workspace_id: &Uuid) -> Result { - Ok(DEFAULT_AI_MODEL_NAME.to_string()) - } -} - -fn chat_message_from_row(row: ChatMessageTable) -> ChatMessage { - let created_at = Utc - .timestamp_opt(row.created_at, 0) - .single() - .unwrap_or_else(Utc::now); - - let author_id = row.author_id.parse::().unwrap_or_default(); - let author_type = match row.author_type { - 1 => ChatAuthorType::Human, - 2 => ChatAuthorType::System, - 3 => ChatAuthorType::AI, - _ => ChatAuthorType::Unknown, - }; - - let metadata = row - .metadata - .map(|s| deserialize_chat_metadata::(&s)) - .unwrap_or_else(|| json!({})); - - ChatMessage { - author: ChatAuthor { - author_id, - author_type, - meta: None, - }, - message_id: row.message_id, - content: row.content, - created_at, - metadata, - reply_message_id: row.reply_message_id, - } -} diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index ad1184a09a..d22088a2c4 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,64 +1,63 @@ -#![allow(unused_variables)] - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::util::default_encode_collab_for_collab_type; use collab::entity::EncodedCollab; +use collab_database::database::default_database_data; +use collab_database::workspace_database::default_workspace_database_data; +use collab_document::document_data::default_document_collab_data; use collab_entity::CollabType; +use collab_user::core::default_user_awareness_data; use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; -pub(crate) struct LocalServerDatabaseCloudServiceImpl { - pub logged_user: Arc, -} +pub(crate) struct LocalServerDatabaseCloudServiceImpl(); #[async_trait] impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { async fn get_database_encode_collab( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - _workspace_id: &Uuid, // underscore to silence “unused” warning + _workspace_id: &str, ) -> Result, FlowyError> { - let uid = self.logged_user.user_id()?; - let object_id = object_id.to_string(); - default_encode_collab_for_collab_type(uid, &object_id, collab_type) - .await - .map(Some) - .or_else(|err| { - if matches!(err.code, ErrorCode::NotSupportYet) { - Ok(None) - } else { - Err(err) - } - }) + match collab_type { + CollabType::Document => { + let encode_collab = default_document_collab_data(object_id)?; + Ok(Some(encode_collab)) + }, + CollabType::Database => default_database_data(object_id) + .await + .map(Some) + .map_err(Into::into), + CollabType::WorkspaceDatabase => Ok(Some(default_workspace_database_data(object_id))), + CollabType::Folder => Ok(None), + CollabType::DatabaseRow => Ok(None), + CollabType::UserAwareness => Ok(Some(default_user_awareness_data(object_id))), + CollabType::Unknown => Ok(None), + } } async fn create_database_encode_collab( &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - encoded_collab: EncodedCollab, + _object_id: &str, + _collab_type: CollabType, + _workspace_id: &str, + _encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } async fn batch_get_database_encode_collab( &self, - object_ids: Vec, - object_ty: CollabType, - workspace_id: &Uuid, + _object_ids: Vec, + _object_ty: CollabType, + _workspace_id: &str, ) -> Result { Ok(EncodeCollabByOid::default()) } async fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, - limit: usize, + _object_id: &str, + _limit: usize, ) -> Result, FlowyError> { Ok(vec![]) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index c553026274..152dcb78d8 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,9 +1,7 @@ -#![allow(unused_variables)] use collab::entity::EncodedCollab; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub(crate) struct LocalServerDocumentCloudServiceImpl(); @@ -11,8 +9,8 @@ pub(crate) struct LocalServerDocumentCloudServiceImpl(); impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { async fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, + document_id: &str, + _workspace_id: &str, ) -> Result, FlowyError> { let document_id = document_id.to_string(); @@ -24,26 +22,26 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { async fn get_document_snapshots( &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, + _document_id: &str, + _limit: usize, + _workspace_id: &str, ) -> Result, FlowyError> { Ok(vec![]) } async fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, + _document_id: &str, + _workspace_id: &str, ) -> Result, FlowyError> { Ok(None) } async fn create_document_collab( &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, + _workspace_id: &str, + _document_id: &str, + _encoded_collab: EncodedCollab, ) -> Result<(), FlowyError> { Ok(()) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 79b1d4be12..52d9a9e98c 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,30 +1,50 @@ -#![allow(unused_variables)] +use std::sync::Arc; -use crate::af_cloud::define::LoggedUser; -use crate::local_server::util::default_encode_collab_for_collab_type; use client_api::entity::workspace_dto::PublishInfoView; use client_api::entity::PublishInfo; -use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; use collab_entity::CollabType; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; + +use crate::local_server::LocalServerDB; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, + gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, + FullSyncCollabParams, Workspace, WorkspaceRecord, }; use flowy_folder_pub::entities::PublishPayload; use lib_infra::async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; pub(crate) struct LocalServerFolderCloudServiceImpl { #[allow(dead_code)] - pub logged_user: Arc, + pub db: Arc, } #[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { + async fn create_workspace(&self, uid: i64, name: &str) -> Result { + let name = name.to_string(); + Ok(Workspace::new( + gen_workspace_id().to_string(), + name.to_string(), + uid, + )) + } + + async fn open_workspace(&self, _workspace_id: &str) -> Result<(), FlowyError> { + Ok(()) + } + + async fn get_all_workspace(&self) -> Result, FlowyError> { + Ok(vec![]) + } + + async fn get_folder_data( + &self, + _workspace_id: &str, + _uid: &i64, + ) -> Result, FlowyError> { + Ok(None) + } + async fn get_folder_snapshots( &self, _workspace_id: &str, @@ -35,46 +55,18 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn get_folder_doc_state( &self, - workspace_id: &Uuid, - uid: i64, - collab_type: CollabType, - object_id: &Uuid, + _workspace_id: &str, + _uid: i64, + _collab_type: CollabType, + _object_id: &str, ) -> Result, FlowyError> { - let object_id = object_id.to_string(); - let workspace_id = workspace_id.to_string(); - let collab_db = self.logged_user.get_collab_db(uid)?.upgrade().unwrap(); - let read_txn = collab_db.read_txn(); - let is_exist = read_txn.is_exist(uid, &workspace_id.to_string(), &object_id.to_string()); - if is_exist { - // load doc - let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); - read_txn.load_doc(uid, &workspace_id, &object_id, collab.doc())?; - let data = collab.encode_collab_v1(|c| { - collab_type - .validate_require_data(c) - .map_err(|err| FlowyError::invalid_data().with_context(err))?; - Ok::<_, FlowyError>(()) - })?; - Ok(data.doc_state.to_vec()) - } else { - let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; - drop(read_txn); - Ok(data.doc_state.to_vec()) - } - } - - async fn full_sync_collab_object( - &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - Ok(()) + Err(FlowyError::local_version_not_support()) } async fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, - objects: Vec, + _workspace_id: &str, + _objects: Vec, ) -> Result<(), FlowyError> { Ok(()) } @@ -85,72 +77,80 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { async fn publish_view( &self, - workspace_id: &Uuid, - payload: Vec, + _workspace_id: &str, + _payload: Vec, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } async fn unpublish_views( &self, - workspace_id: &Uuid, - view_ids: Vec, + _workspace_id: &str, + _view_ids: Vec, ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_publish_info(&self, view_id: &Uuid) -> Result { Err(FlowyError::local_version_not_support()) } - async fn set_publish_name( - &self, - workspace_id: &Uuid, - view_id: Uuid, - new_name: String, - ) -> Result<(), FlowyError> { + async fn get_publish_info(&self, _view_id: &str) -> Result { Err(FlowyError::local_version_not_support()) } async fn set_publish_namespace( &self, - workspace_id: &Uuid, - new_namespace: String, + _workspace_id: &str, + _new_namespace: String, + ) -> Result<(), FlowyError> { + Err(FlowyError::local_version_not_support()) + } + + async fn get_publish_namespace(&self, _workspace_id: &str) -> Result { + Err(FlowyError::local_version_not_support()) + } + + async fn set_publish_name( + &self, + _workspace_id: &str, + _view_id: String, + _new_name: String, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } async fn list_published_views( &self, - workspace_id: &Uuid, + _workspace_id: &str, ) -> Result, FlowyError> { Err(FlowyError::local_version_not_support()) } async fn get_default_published_view_info( &self, - workspace_id: &Uuid, + _workspace_id: &str, ) -> Result { Err(FlowyError::local_version_not_support()) } async fn set_default_published_view( &self, - workspace_id: &Uuid, - view_id: uuid::Uuid, + _workspace_id: &str, + _view_id: uuid::Uuid, ) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result { + async fn remove_default_published_view(&self, _workspace_id: &str) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } async fn import_zip(&self, _file_path: &str) -> Result<(), FlowyError> { Err(FlowyError::local_version_not_support()) } + + async fn full_sync_collab_object( + &self, + _workspace_id: &str, + _params: FullSyncCollabParams, + ) -> Result<(), FlowyError> { + Ok(()) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs index f63265e734..0280cfbefb 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs @@ -1,10 +1,8 @@ -pub(crate) use chat::*; pub(crate) use database::*; pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; -mod chat; mod database; mod document; mod folder; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index f011c16d90..c800cc7ced 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,48 +1,40 @@ -#![allow(unused_variables)] - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::uid::UserIDGenerator; -use anyhow::Context; -use client_api::entity::GotrueTokenResponse; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::CollabObject; use collab_user::core::UserAwareness; -use flowy_ai_pub::cloud::billing_dto::WorkspaceUsageAndLimit; -use flowy_ai_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; -use flowy_error::FlowyError; -use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; -use flowy_user_pub::entities::*; -use flowy_user_pub::sql::{ - insert_local_workspace, select_all_user_workspace, select_user_profile, select_user_workspace, - select_workspace_member, select_workspace_setting, update_user_profile, update_workspace_setting, - upsert_workspace_member, upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, - WorkspaceSettingsChangeset, WorkspaceSettingsTable, -}; -use flowy_user_pub::DEFAULT_USER_NAME; use lazy_static::lazy_static; -use lib_infra::async_trait::async_trait; -use lib_infra::box_any::BoxAny; -use lib_infra::util::timestamp; use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; +use flowy_error::FlowyError; +use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; +use flowy_user_pub::entities::*; +use flowy_user_pub::DEFAULT_USER_NAME; +use lib_infra::async_trait::async_trait; +use lib_infra::box_any::BoxAny; +use lib_infra::util::timestamp; + +use crate::local_server::uid::UserIDGenerator; +use crate::local_server::LocalServerDB; + lazy_static! { + //FIXME: seriously, userID generation should work using lock-free algorithm static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserServiceImpl { - pub logged_user: Arc, +pub(crate) struct LocalServerUserAuthServiceImpl { + #[allow(dead_code)] + pub db: Arc, } #[async_trait] -impl UserCloudService for LocalServerUserServiceImpl { +impl UserCloudService for LocalServerUserAuthServiceImpl { async fn sign_up(&self, params: BoxAny) -> Result { let params = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let workspace_id = Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new_local(workspace_id, "My Workspace"); + let workspace_id = uuid::Uuid::new_v4().to_string(); + let user_workspace = UserWorkspace::new_local(&workspace_id, uid); let user_name = if params.name.is_empty() { DEFAULT_USER_NAME() } else { @@ -55,8 +47,7 @@ impl UserCloudService for LocalServerUserServiceImpl { latest_workspace: user_workspace.clone(), user_workspaces: vec![user_workspace], is_new_user: true, - // Anon user doesn't have email - email: None, + email: Some(params.email), token: None, encryption_type: EncryptionType::NoEncryption, updated_at: timestamp(), @@ -65,11 +56,13 @@ impl UserCloudService for LocalServerUserServiceImpl { } async fn sign_in(&self, params: BoxAny) -> Result { + let db = self.db.clone(); let params: SignInParams = params.unbox_or_error::()?; let uid = ID_GEN.lock().await.next_id(); - let workspace_id = Uuid::new_v4(); - let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), "My Workspace"); + let user_workspace = db + .get_user_workspace(uid)? + .unwrap_or_else(make_user_workspace); Ok(AuthResponse { user_id: uid, user_uuid: Uuid::new_v4(), @@ -104,7 +97,7 @@ impl UserCloudService for LocalServerUserServiceImpl { &self, _email: &str, _password: &str, - ) -> Result { + ) -> Result { Err(FlowyError::local_version_not_support().with_context("Not support")) } @@ -116,98 +109,58 @@ impl UserCloudService for LocalServerUserServiceImpl { Err(FlowyError::local_version_not_support().with_context("Not support")) } - async fn sign_in_with_passcode( - &self, - _email: &str, - _passcode: &str, - ) -> Result { - Err(FlowyError::local_version_not_support().with_context("Not support")) - } - async fn generate_oauth_url_with_provider(&self, _provider: &str) -> Result { Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let changeset = UserTableChangeset::new(params); - update_user_profile(&mut conn, changeset)?; - Ok(()) - } - - async fn get_user_profile( + async fn update_user( &self, - uid: i64, - workspace_id: &str, - ) -> Result { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, workspace_id, &mut conn)?; - Ok(profile) - } - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; - Ok(UserWorkspace::from(workspace)) - } - - async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError> { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let workspaces = select_all_user_workspace(uid, &mut conn)?; - Ok(workspaces) - } - - async fn create_workspace(&self, workspace_name: &str) -> Result { - let workspace_id = Uuid::new_v4(); - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let user_workspace = - insert_local_workspace(uid, &workspace_id.to_string(), workspace_name, &mut conn)?; - Ok(user_workspace) - } - - async fn patch_workspace( - &self, - workspace_id: &Uuid, - new_workspace_name: Option, - new_workspace_icon: Option, + _credential: UserCredentials, + _params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { Ok(()) } - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Ok(()) + async fn get_user_profile(&self, credential: UserCredentials) -> Result { + match credential.uid { + None => Err(FlowyError::record_not_found()), + Some(uid) => { + self.db.get_user_profile(uid).map(|mut profile| { + // We don't want to expose the email in the local server + profile.email = "".to_string(); + profile + }) + }, + } } - async fn get_workspace_members( - &self, - workspace_id: Uuid, - ) -> Result, FlowyError> { - let uid = self.logged_user.user_id()?; - let member = self.get_workspace_member(&workspace_id, uid).await?; - Ok(vec![member]) + async fn open_workspace(&self, _workspace_id: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support open workspace"), + ) + } + + async fn get_all_workspace(&self, _uid: i64) -> Result, FlowyError> { + Ok(vec![]) } async fn get_user_awareness_doc_state( &self, - uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, + _uid: i64, + _workspace_id: &str, + object_id: &str, ) -> Result, FlowyError> { - let collab = Collab::new_with_origin( - CollabOrigin::Empty, - object_id.to_string().as_str(), - vec![], - false, - ); + let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); let awareness = UserAwareness::create(collab, None)?; let encode_collab = awareness.encode_collab_v1(|_collab| Ok::<_, FlowyError>(()))?; Ok(encode_collab.doc_state.to_vec()) } + async fn reset_workspace(&self, _collab_object: CollabObject) -> Result<(), FlowyError> { + Ok(()) + } + async fn create_collab_object( &self, _collab_object: &CollabObject, @@ -218,123 +171,50 @@ impl UserCloudService for LocalServerUserServiceImpl { async fn batch_create_collab_object( &self, - workspace_id: &Uuid, - objects: Vec, + _workspace_id: &str, + _objects: Vec, ) -> Result<(), FlowyError> { - Ok(()) + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support batch create collab object"), + ) } - async fn get_workspace_member( - &self, - workspace_id: &Uuid, - uid: i64, - ) -> Result { - // For local server, only current user is the member - let conn = self.logged_user.get_sqlite_db(uid)?; - let result = select_workspace_member(conn, &workspace_id.to_string(), uid); - - match result { - Ok(row) => Ok(WorkspaceMember::from(row)), - Err(err) => { - if err.is_record_not_found() { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn) - .context("Can't find user profile when create workspace member")?; - let row = WorkspaceMemberTable { - email: profile.email.to_string(), - role: Role::Owner as i32, - name: profile.name.to_string(), - avatar_url: Some(profile.icon_url), - uid, - workspace_id: workspace_id.to_string(), - updated_at: chrono::Utc::now().naive_utc(), - }; - - let member = WorkspaceMember::from(row.clone()); - upsert_workspace_member(&mut conn, row)?; - Ok(member) - } else { - Err(err) - } - }, - } + async fn create_workspace(&self, _workspace_name: &str) -> Result { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) } - async fn get_workspace_usage( - &self, - workspace_id: &Uuid, - ) -> Result { - Ok(WorkspaceUsageAndLimit { - member_count: 1, - member_count_limit: 1, - storage_bytes: i64::MAX, - storage_bytes_limit: i64::MAX, - storage_bytes_unlimited: true, - single_upload_limit: i64::MAX, - single_upload_unlimited: true, - ai_responses_count: i64::MAX, - ai_responses_count_limit: i64::MAX, - ai_image_responses_count: i64::MAX, - ai_image_responses_count_limit: 0, - local_ai: true, - ai_responses_unlimited: true, - }) + async fn delete_workspace(&self, _workspace_id: &str) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) } - async fn get_workspace_setting( + async fn patch_workspace( &self, - workspace_id: &Uuid, - ) -> Result { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - // By default, workspace setting is existed in local server - let result = select_workspace_setting(&mut conn, &workspace_id.to_string()); - match result { - Ok(row) => Ok(AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model, - }), - Err(err) => { - if err.is_record_not_found() { - let row = WorkspaceSettingsTable { - id: workspace_id.to_string(), - disable_search_indexing: false, - ai_model: "".to_string(), - }; - let setting = AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model.clone(), - }; - upsert_workspace_setting(&mut conn, row)?; - Ok(setting) - } else { - Err(err) - } - }, - } - } - - async fn update_workspace_setting( - &self, - workspace_id: &Uuid, - workspace_settings: AFWorkspaceSettingsChange, - ) -> Result { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - let changeset = WorkspaceSettingsChangeset { - id: workspace_id.to_string(), - disable_search_indexing: workspace_settings.disable_search_indexing, - ai_model: workspace_settings.ai_model, - }; - - update_workspace_setting(&mut conn, changeset)?; - let row = select_workspace_setting(&mut conn, &workspace_id.to_string())?; - - Ok(AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model, - }) + _workspace_id: &str, + _new_workspace_name: Option<&str>, + _new_workspace_icon: Option<&str>, + ) -> Result<(), FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + } +} + +fn make_user_workspace() -> UserWorkspace { + UserWorkspace { + id: uuid::Uuid::new_v4().to_string(), + name: "My Workspace".to_string(), + created_at: Default::default(), + workspace_database_id: uuid::Uuid::new_v4().to_string(), + icon: "".to_string(), + member_count: 1, + role: None, } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/mod.rs index 2b9fe07250..6e67356fd9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/mod.rs @@ -3,4 +3,3 @@ pub use server::*; pub mod impls; mod server; pub(crate) mod uid; -mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 8829ded3fc..cb8b545c53 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,32 +1,39 @@ -use crate::af_cloud::define::LoggedUser; -use crate::local_server::impls::{ - LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, - LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, -}; -use crate::AppFlowyServer; -use anyhow::Error; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; -use flowy_document_pub::cloud::DocumentCloudService; -use flowy_folder_pub::cloud::FolderCloudService; use flowy_search_pub::cloud::SearchCloudService; -use flowy_storage_pub::cloud::StorageCloudService; -use flowy_user_pub::cloud::UserCloudService; use std::sync::Arc; + use tokio::sync::mpsc; +use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; +use flowy_document_pub::cloud::DocumentCloudService; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::FolderCloudService; +use flowy_storage_pub::cloud::StorageCloudService; +// use flowy_user::services::database::{ +// get_user_profile, get_user_workspace, open_collab_db, open_user_db, +// }; +use flowy_user_pub::cloud::UserCloudService; +use flowy_user_pub::entities::*; + +use crate::local_server::impls::{ + LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, + LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl, +}; +use crate::AppFlowyServer; + +pub trait LocalServerDB: Send + Sync + 'static { + fn get_user_profile(&self, uid: i64) -> Result; + fn get_user_workspace(&self, uid: i64) -> Result, FlowyError>; +} + pub struct LocalServer { - logged_user: Arc, - local_ai: Arc, + local_db: Arc, stop_tx: Option>, } impl LocalServer { - pub fn new(logged_user: Arc, local_ai: Arc) -> Self { + pub fn new(local_db: Arc) -> Self { Self { - logged_user, - local_ai, + local_db, stop_tx: Default::default(), } } @@ -40,48 +47,35 @@ impl LocalServer { } impl AppFlowyServer for LocalServer { - fn set_token(&self, _token: &str) -> Result<(), Error> { - Ok(()) - } - fn user_service(&self) -> Arc { - Arc::new(LocalServerUserServiceImpl { - logged_user: self.logged_user.clone(), + Arc::new(LocalServerUserAuthServiceImpl { + db: self.local_db.clone(), }) } fn folder_service(&self) -> Arc { Arc::new(LocalServerFolderCloudServiceImpl { - logged_user: self.logged_user.clone(), + db: self.local_db.clone(), }) } fn database_service(&self) -> Arc { - Arc::new(LocalServerDatabaseCloudServiceImpl { - logged_user: self.logged_user.clone(), - }) - } - - fn database_ai_service(&self) -> Option> { - None + Arc::new(LocalServerDatabaseCloudServiceImpl()) } fn document_service(&self) -> Arc { Arc::new(LocalServerDocumentCloudServiceImpl()) } - fn chat_service(&self) -> Arc { - Arc::new(LocalChatServiceImpl { - logged_user: self.logged_user.clone(), - local_ai: self.local_ai.clone(), - }) + fn file_storage(&self) -> Option> { + None } fn search_service(&self) -> Option> { None } - fn file_storage(&self) -> Option> { + fn database_ai_service(&self) -> Option> { None } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs deleted file mode 100644 index 378ccee6a2..0000000000 --- a/frontend/rust-lib/flowy-server/src/local_server/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -use collab::core::origin::CollabOrigin; -use collab::entity::EncodedCollab; -use collab::preclude::Collab; -use collab_database::database::default_database_data; -use collab_database::workspace_database::default_workspace_database_data; -use collab_document::document_data::default_document_collab_data; -use collab_entity::CollabType; -use collab_user::core::default_user_awareness_data; -use flowy_error::{FlowyError, FlowyResult}; - -pub async fn default_encode_collab_for_collab_type( - _uid: i64, - object_id: &str, - collab_type: CollabType, -) -> FlowyResult { - match collab_type { - CollabType::Document => { - let encode_collab = default_document_collab_data(object_id)?; - Ok(encode_collab) - }, - CollabType::Database => default_database_data(object_id).await.map_err(Into::into), - CollabType::WorkspaceDatabase => Ok(default_workspace_database_data(object_id)), - CollabType::Folder => { - // let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - // let workspace = Workspace::new(object_id.to_string(), "".to_string(), uid); - // let folder_data = FolderData::new(workspace); - // let folder = Folder::create(uid, collab, None, folder_data); - // let data = folder.encode_collab_v1(|c| { - // collab_type - // .validate_require_data(c) - // .map_err(|err| FlowyError::invalid_data().with_context(err))?; - // Ok::<_, FlowyError>(()) - // })?; - // Ok(data) - Err(FlowyError::not_support().with_context("Can not create default folder")) - }, - CollabType::DatabaseRow => { - Err(FlowyError::not_support().with_context("Can not create default database row")) - }, - CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), - CollabType::Unknown => { - let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - let data = collab.encode_collab_v1(|_| Ok::<_, FlowyError>(()))?; - Ok(data) - }, - } -} diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 2702b4f104..ee07eefa5a 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -12,6 +12,7 @@ use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage}; +use crate::default_impl::DefaultChatCloudServiceImpl; use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; @@ -41,7 +42,9 @@ where /// and functionalities in AppFlowy. The methods provided ensure efficient, asynchronous operations /// for managing and accessing user data, folders, collaborative objects, and documents in a cloud environment. pub trait AppFlowyServer: Send + Sync + 'static { - fn set_token(&self, _token: &str) -> Result<(), Error>; + fn set_token(&self, _token: &str) -> Result<(), Error> { + Ok(()) + } fn set_ai_model(&self, _ai_model: &str) -> Result<(), Error> { Ok(()) @@ -100,7 +103,9 @@ pub trait AppFlowyServer: Send + Sync + 'static { /// An `Arc` wrapping the `DocumentCloudService` interface. fn document_service(&self) -> Arc; - fn chat_service(&self) -> Arc; + fn chat_service(&self) -> Arc { + Arc::new(DefaultChatCloudServiceImpl) + } /// Bridge for the Cloud AI Search features /// diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 3712307af4..65d02c704a 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -199,16 +199,6 @@ where }) } - fn sign_in_with_passcode( - &self, - _email: &str, - _passcode: &str, - ) -> FutureResult { - FutureResult::new(async { - Err(FlowyError::not_support().with_context("Can't sign in with passcode when using supabase")) - }) - } - fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult { FutureResult::new(async { Err(FlowyError::internal().with_context("Can't generate oauth url when using supabase")) diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs new file mode 100644 index 0000000000..94ad2e2e1d --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs @@ -0,0 +1,2 @@ +mod user_test; +mod util; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs new file mode 100644 index 0000000000..a14d8eaf25 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs @@ -0,0 +1,21 @@ +use flowy_server::AppFlowyServer; +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::af_cloud_test::util::{ + af_cloud_server, af_cloud_sign_up_param, generate_test_email, get_af_cloud_config, +}; + +#[tokio::test] +async fn sign_up_test() { + if let Some(config) = get_af_cloud_config() { + let server = af_cloud_server(config.clone()); + let user_service = server.user_service(); + let email = generate_test_email(); + let params = af_cloud_sign_up_param(&email, &config).await; + let resp: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert_eq!(resp.email.unwrap(), email); + assert!(resp.is_new_user); + assert_eq!(resp.user_workspaces.len(), 1); + } +} diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs new file mode 100644 index 0000000000..ecf34ec31d --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -0,0 +1,93 @@ +use client_api::ClientConfiguration; +use semver::Version; +use std::collections::HashMap; +use std::sync::Arc; + +use flowy_error::FlowyResult; +use uuid::Uuid; + +use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; + +use crate::setup_log; + +/// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: +/// +/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000` +/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws` +/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998` +/// +/// - `GOTRUE_ADMIN_EMAIL=admin@example.com` +/// - `GOTRUE_ADMIN_PASSWORD=password` +pub fn get_af_cloud_config() -> Option { + dotenv::from_filename("./.env.ci").ok()?; + setup_log(); + AFCloudConfiguration::from_env().ok() +} + +pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc { + let fake_device_id = uuid::Uuid::new_v4().to_string(); + Arc::new(AppFlowyCloudServer::new( + config, + true, + fake_device_id, + Version::new(0, 5, 8), + Arc::new(FakeServerUserImpl), + )) +} + +struct FakeServerUserImpl; +impl ServerUser for FakeServerUserImpl { + fn workspace_id(&self) -> FlowyResult { + todo!() + } +} + +pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { + let client = client_api::Client::new( + &config.base_url, + &config.ws_base_url, + &config.gotrue_url, + "fake_device_id", + ClientConfiguration::default(), + "test", + ); + let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); + let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); + let admin_client = client_api::Client::new( + client.base_url(), + client.ws_addr(), + client.gotrue_url(), + "fake_device_id", + ClientConfiguration::default(), + &client.client_version.to_string(), + ); + admin_client + .sign_in_password(&admin_email, &admin_password) + .await + .unwrap(); + + let action_link = admin_client + .generate_sign_in_action_link(user_email) + .await + .unwrap(); + client.extract_sign_in_url(&action_link).await.unwrap() +} + +pub async fn af_cloud_sign_up_param( + email: &str, + config: &AFCloudConfiguration, +) -> HashMap { + let mut params = HashMap::new(); + params.insert( + "sign_in_url".to_string(), + generate_sign_in_url(email, config).await, + ); + params.insert("device_id".to_string(), Uuid::new_v4().to_string()); + params +} + +pub fn generate_test_email() -> String { + format!("{}@test.com", Uuid::new_v4()) +} diff --git a/frontend/rust-lib/flowy-server/tests/logo.png b/frontend/rust-lib/flowy-server/tests/logo.png new file mode 100644 index 0000000000..d6f09e3e2e Binary files /dev/null and b/frontend/rust-lib/flowy-server/tests/logo.png differ diff --git a/frontend/rust-lib/flowy-server/tests/main.rs b/frontend/rust-lib/flowy-server/tests/main.rs new file mode 100644 index 0000000000..fb12ed51b3 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/main.rs @@ -0,0 +1,24 @@ +use std::sync::Once; + +use tracing_subscriber::fmt::Subscriber; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +mod af_cloud_test; +// mod supabase_test; + +pub fn setup_log() { + static START: Once = Once::new(); + START.call_once(|| { + let level = "trace"; + let mut filters = vec![]; + filters.push(format!("flowy_server={}", level)); + std::env::set_var("RUST_LOG", filters.join(",")); + + let subscriber = Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_ansi(true) + .finish(); + subscriber.try_init().unwrap(); + }); +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs new file mode 100644 index 0000000000..841c76b443 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs @@ -0,0 +1,63 @@ +use collab::core::collab::DataSource; +use collab_entity::{CollabObject, CollabType}; +use uuid::Uuid; + +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + collab_service, database_service, get_supabase_ci_config, third_party_sign_up_param, + user_auth_service, +}; + +#[tokio::test] +async fn supabase_create_database_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_service = collab_service(); + let database_service = database_service(); + + let mut row_ids = vec![]; + for _i in 0..3 { + let row_id = uuid::Uuid::new_v4().to_string(); + row_ids.push(row_id.clone()); + let collab_object = CollabObject::new( + user.user_id, + row_id, + CollabType::DatabaseRow, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + collab_service + .send_update(&collab_object, 0, vec![1, 2, 3]) + .await + .unwrap(); + collab_service + .send_update(&collab_object, 0, vec![4, 5, 6]) + .await + .unwrap(); + } + + let updates_by_oid = database_service + .batch_get_database_object_doc_state(row_ids, CollabType::DatabaseRow, "fake_workspace_id") + .await + .unwrap(); + + assert_eq!(updates_by_oid.len(), 3); + for (_, source) in updates_by_oid { + match source { + DataSource::Disk => panic!("should not be from disk"), + DataSource::DocStateV1(doc_state) => { + assert_eq!(doc_state.len(), 2); + }, + DataSource::DocStateV2(_) => {}, + } + } +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs new file mode 100644 index 0000000000..4377ce8e68 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs @@ -0,0 +1,78 @@ +// use url::Url; +// use uuid::Uuid; +// +// use flowy_storage::StorageObject; +// +// use crate::supabase_test::util::{file_storage_service, get_supabase_ci_config}; +// +// #[tokio::test] +// async fn supabase_get_object_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("test-{}.txt", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); +// +// // Upload a file +// let url = service +// .create_object(object) +// .await +// .unwrap() +// .parse::() +// .unwrap(); +// +// // The url would be something like: +// // https://acfrqdbdtbsceyjbxsfc.supabase.co/storage/v1/object/data/test-1693472809.txt +// let name = url.path_segments().unwrap().last().unwrap(); +// assert_eq!(name, &file_name); +// +// // Download the file +// let bytes = service.get_object(url.to_string()).await.unwrap(); +// let s = String::from_utf8(bytes.to_vec()).unwrap(); +// assert_eq!(s, "hello world"); +// } +// +// #[tokio::test] +// async fn supabase_upload_image_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("image-{}.png", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/logo.png"); +// +// // Upload a file +// let url = service +// .create_object(object) +// .await +// .unwrap() +// .parse::() +// .unwrap(); +// +// // Download object by url +// let bytes = service.get_object(url.to_string()).await.unwrap(); +// assert_eq!(bytes.len(), 15694); +// } +// +// #[tokio::test] +// async fn supabase_delete_object_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("test-{}.txt", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); +// let url = service.create_object(object).await.unwrap(); +// +// let result = service.get_object(url.clone()).await; +// assert!(result.is_ok()); +// +// let _ = service.delete_object(url.clone()).await; +// +// let result = service.get_object(url.clone()).await; +// assert!(result.is_err()); +// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs new file mode 100644 index 0000000000..a9037caa6c --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs @@ -0,0 +1,316 @@ +use assert_json_diff::assert_json_eq; +use collab_entity::{CollabObject, CollabType}; +use serde_json::json; +use uuid::Uuid; +use yrs::types::ToJson; +use yrs::updates::decoder::Decode; +use yrs::{merge_updates_v1, Array, Doc, Map, MapPrelim, ReadTxn, StateVector, Transact, Update}; + +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + collab_service, folder_service, get_supabase_ci_config, third_party_sign_up_param, + user_auth_service, +}; + +#[tokio::test] +async fn supabase_create_workspace_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let service = folder_service(); + // will replace the uid with the real uid + let workspace = service.create_workspace(1, "test").await.unwrap(); + dbg!(workspace); +} + +#[tokio::test] +async fn supabase_get_folder_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + collab_service + .send_update(&collab_object, 0, txn.encode_update_v1()) + .await + .unwrap(); + }; + + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + collab_service + .send_update(&collab_object, 1, txn.encode_update_v1()) + .await + .unwrap(); + }; + + // let updates = collab_service.get_all_updates(&collab_object).await.unwrap(); + let updates = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + assert_eq!(updates.len(), 2); + + for _ in 0..5 { + collab_service + .send_init_sync(&collab_object, 3, vec![]) + .await + .unwrap(); + } + let updates = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + // Other the init sync, try to get the updates from the server. + let expected_update = doc + .transact_mut() + .encode_state_as_update_v1(&StateVector::default()); + + // check the update is the same as local document update. + assert_eq!(updates, expected_update); +} + +/// This async test function checks the behavior of updates duplication in Supabase. +/// It creates a new user and simulates two updates to the user's workspace with different values. +/// Then, it merges these updates and sends an initial synchronization request to test duplication handling. +/// Finally, it asserts that the duplicated updates don't affect the overall data consistency in Supabase. +#[tokio::test] +async fn supabase_duplicate_updates_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + let mut duplicated_updates = vec![]; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + let update = txn.encode_update_v1(); + duplicated_updates.push(update.clone()); + collab_service + .send_update(&collab_object, 0, update) + .await + .unwrap(); + }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + let update = txn.encode_update_v1(); + duplicated_updates.push(update.clone()); + collab_service + .send_update(&collab_object, 1, update) + .await + .unwrap(); + }; + // send init sync + collab_service + .send_init_sync(&collab_object, 3, vec![]) + .await + .unwrap(); + let first_init_sync_update = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + // simulate the duplicated updates. + let merged_update = merge_updates_v1( + &duplicated_updates + .iter() + .map(|update| update.as_ref()) + .collect::>(), + ) + .unwrap(); + collab_service + .send_init_sync(&collab_object, 4, merged_update) + .await + .unwrap(); + let second_init_sync_update = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + let doc_2 = Doc::new(); + assert_eq!(first_init_sync_update.len(), second_init_sync_update.len()); + let map = { doc_2.get_or_insert_map("map") }; + { + let mut txn = doc_2.transact_mut(); + let update = Update::decode_v1(&second_init_sync_update).unwrap(); + txn.apply_update(update).unwrap(); + } + { + let txn = doc_2.transact(); + let json = map.to_json(&txn); + assert_json_eq!( + json, + json!({ + "1": "a", + "2": "b" + }) + ); + } +} + +/// The state vector of doc; +/// ```json +/// "map": {}, +/// "array": [] +/// ``` +/// The old version of doc: +/// ```json +/// "map": {} +/// ``` +/// +/// Try to apply the updates from doc to old version doc and check the result. +#[tokio::test] +async fn supabase_diff_state_vector_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + let array = { doc.get_or_insert_array("array") }; + + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + map.insert(&mut txn, "inner_map", MapPrelim::::new()); + + array.push_back(&mut txn, "element 1"); + let update = txn.encode_update_v1(); + collab_service + .send_update(&collab_object, 0, update) + .await + .unwrap(); + }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + array.push_back(&mut txn, "element 2"); + let update = txn.encode_update_v1(); + collab_service + .send_update(&collab_object, 1, update) + .await + .unwrap(); + }; + + // restore the doc with given updates. + let old_version_doc = Doc::new(); + let map = { old_version_doc.get_or_insert_map("map") }; + let doc_state = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + { + let mut txn = old_version_doc.transact_mut(); + let update = Update::decode_v1(&doc_state).unwrap(); + txn.apply_update(update).unwrap(); + } + let txn = old_version_doc.transact(); + let json = map.to_json(&txn); + assert_json_eq!( + json, + json!({ + "1": "a", + "2": "b", + "inner_map": {} + }) + ); +} + +// #[tokio::test] +// async fn print_folder_object_test() { +// if get_supabase_dev_config().is_none() { +// return; +// } +// let secret = Some("43bSxEPHeNkk5ZxxEYOfAjjd7sK2DJ$vVnxwuNc5ru0iKFvhs8wLg==".to_string()); +// print_encryption_folder("f8b14b84-e8ec-4cf4-a318-c1e008ecfdfa", secret).await; +// } +// +// #[tokio::test] +// async fn print_folder_snapshot_object_test() { +// if get_supabase_dev_config().is_none() { +// return; +// } +// let secret = Some("NTXRXrDSybqFEm32jwMBDzbxvCtgjU$8np3TGywbBdJAzHtu1QIyQ==".to_string()); +// // let secret = None; +// print_encryption_folder_snapshot("12533251-bdd4-41f4-995f-ff12fceeaa42", secret).await; +// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs new file mode 100644 index 0000000000..ab82d37866 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs @@ -0,0 +1,5 @@ +mod database_test; +mod file_test; +mod folder_test; +mod user_test; +mod util; diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs new file mode 100644 index 0000000000..13df930601 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs @@ -0,0 +1,141 @@ +use uuid::Uuid; + +use flowy_encrypt::{encrypt_text, generate_encryption_secret}; +use flowy_error::FlowyError; +use flowy_user_pub::entities::*; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + get_supabase_ci_config, third_party_sign_up_param, user_auth_service, +}; + +// ‼️‼️‼️ Warning: this test will create a table in the database +#[tokio::test] +async fn supabase_user_sign_up_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert!(!user.latest_workspace.id.is_empty()); + assert!(!user.user_workspaces.is_empty()); + assert!(!user.latest_workspace.database_indexer_id.is_empty()); +} + +#[tokio::test] +async fn supabase_user_sign_up_with_existing_uuid_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let _user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert!(!user.latest_workspace.id.is_empty()); + assert!(!user.latest_workspace.database_indexer_id.is_empty()); + assert!(!user.user_workspaces.is_empty()); +} + +#[tokio::test] +async fn supabase_update_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + let params = UpdateUserProfileParams::new(user.user_id) + .with_name("123") + .with_email(format!("{}@test.com", Uuid::new_v4())); + + user_service + .update_user(UserCredentials::from_uid(user.user_id), params) + .await + .unwrap(); + + let user_profile = user_service + .get_user_profile(UserCredentials::from_uid(user.user_id)) + .await + .unwrap(); + + assert_eq!(user_profile.name, "123"); +} + +#[tokio::test] +async fn supabase_get_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + let credential = UserCredentials::from_uid(user.user_id); + user_service + .get_user_profile(credential.clone()) + .await + .unwrap(); +} + +#[tokio::test] +async fn supabase_get_not_exist_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let user_service = user_auth_service(); + let result: FlowyError = user_service + .get_user_profile(UserCredentials::from_uid(i64::MAX)) + .await + .unwrap_err(); + // user not found + assert!(result.is_record_not_found()); +} + +#[tokio::test] +async fn user_encryption_sign_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + // generate encryption sign + let secret = generate_encryption_secret(); + let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap(); + + user_service + .update_user( + UserCredentials::from_uid(user.user_id), + UpdateUserProfileParams::new(user.user_id) + .with_encryption_type(EncryptionType::SelfEncryption(sign.clone())), + ) + .await + .unwrap(); + + let user_profile: UserProfile = user_service + .get_user_profile(UserCredentials::from_uid(user.user_id)) + .await + .unwrap(); + assert_eq!( + user_profile.encryption_type, + EncryptionType::SelfEncryption(sign) + ); +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs new file mode 100644 index 0000000000..7fba91fe9a --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::origin::CollabOrigin; +use collab::preclude::Collab; +use collab_plugins::cloud_storage::RemoteCollabStorage; +use uuid::Uuid; + +use flowy_database_pub::cloud::DatabaseCloudService; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::{Folder, FolderCloudService}; +use flowy_server::supabase::api::{ + RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, + SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl, +}; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; +use flowy_server::{AppFlowyEncryption, EncryptionImpl}; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_user_pub::cloud::UserCloudService; +use lib_infra::future::FutureResult; + +use crate::setup_log; + +pub fn get_supabase_ci_config() -> Option { + dotenv::from_filename("./.env.ci").ok()?; + setup_log(); + SupabaseConfiguration::from_env().ok() +} + +#[allow(dead_code)] +pub fn get_supabase_dev_config() -> Option { + dotenv::from_filename("./.env.dev").ok()?; + setup_log(); + SupabaseConfiguration::from_env().ok() +} + +pub fn collab_service() -> Arc { + let (server, encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )) +} + +pub fn database_service() -> Arc { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseDatabaseServiceImpl::new(server)) +} + +pub fn user_auth_service() -> Arc { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseUserServiceImpl::new(server, vec![], None)) +} + +pub fn folder_service() -> Arc { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseFolderServiceImpl::new(server)) +} + +#[allow(dead_code)] +pub fn file_storage_service() -> Arc { + let encryption_impl: Arc = Arc::new(EncryptionImpl::new(None)); + let config = SupabaseConfiguration::from_env().unwrap(); + Arc::new( + SupabaseFileStorage::new( + &config, + Arc::downgrade(&encryption_impl), + Arc::new(TestFileStoragePlan), + ) + .unwrap(), + ) +} + +#[allow(dead_code)] +pub fn encryption_folder_service( + secret: Option, +) -> (Arc, Arc) { + let (server, encryption_impl) = supabase_server_service(secret); + let service = Arc::new(SupabaseFolderServiceImpl::new(server)); + (service, encryption_impl) +} + +#[allow(dead_code)] +pub fn encryption_collab_service( + secret: Option, +) -> (Arc, Arc) { + let (server, encryption_impl) = supabase_server_service(secret); + let service = Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )); + (service, encryption_impl) +} + +#[allow(dead_code)] +pub async fn print_encryption_folder( + uid: &i64, + folder_id: &str, + encryption_secret: Option, +) { + let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); + let folder_data = cloud_service.get_folder_data(folder_id, uid).await.unwrap(); + let json = serde_json::to_value(folder_data).unwrap(); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +#[allow(dead_code)] +pub async fn print_encryption_folder_snapshot( + uid: &i64, + folder_id: &str, + encryption_secret: Option, +) { + let (cloud_service, _encryption) = encryption_collab_service(encryption_secret); + let snapshot = cloud_service + .get_snapshots(folder_id, 1) + .await + .pop() + .unwrap(); + let collab = Arc::new(MutexCollab::new( + Collab::new_with_source( + CollabOrigin::Empty, + folder_id, + DataSource::DocStateV1(snapshot.blob), + vec![], + false, + ) + .unwrap(), + )); + let folder_data = Folder::open(uid, collab, None) + .unwrap() + .get_folder_data(folder_id) + .unwrap(); + let json = serde_json::to_value(folder_data).unwrap(); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +pub fn supabase_server_service( + encryption_secret: Option, +) -> (SupabaseServerServiceImpl, Arc) { + let config = SupabaseConfiguration::from_env().unwrap(); + let encryption_impl: Arc = + Arc::new(EncryptionImpl::new(encryption_secret)); + let encryption = Arc::downgrade(&encryption_impl); + let server = Arc::new(RESTfulPostgresServer::new(config, encryption)); + (SupabaseServerServiceImpl::new(server), encryption_impl) +} + +pub fn third_party_sign_up_param(uuid: String) -> HashMap { + let mut params = HashMap::new(); + params.insert(USER_UUID.to_string(), uuid); + params.insert( + USER_EMAIL.to_string(), + format!("{}@test.com", Uuid::new_v4()), + ); + params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); + params +} + +pub struct TestFileStoragePlan; diff --git a/frontend/rust-lib/flowy-server/tests/test.txt b/frontend/rust-lib/flowy-server/tests/test.txt new file mode 100644 index 0000000000..95d09f2b10 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 345b05f903..0e85aebee5 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] diesel.workspace = true -diesel_derives = { workspace = true, features = ["sqlite", "r2d2"] } +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] } tracing.workspace = true serde.workspace = true diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql deleted file mode 100644 index 8b07e6189d..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql +++ /dev/null @@ -1,9 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE chat_table - ADD COLUMN local_enabled INTEGER; -ALTER TABLE chat_table - ADD COLUMN sync_to_cloud INTEGER; -ALTER TABLE chat_table - ADD COLUMN local_files TEXT; - -ALTER TABLE chat_table DROP COLUMN rag_ids; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql deleted file mode 100644 index 0604601486..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE chat_table DROP COLUMN local_enabled; -ALTER TABLE chat_table DROP COLUMN local_files; -ALTER TABLE chat_table DROP COLUMN sync_to_cloud; -ALTER TABLE chat_table ADD COLUMN rag_ids TEXT; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql deleted file mode 100644 index 65dec0f30a..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE chat_table DROP COLUMN is_sync; -ALTER TABLE chat_message_table DROP COLUMN is_sync; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql deleted file mode 100644 index ff8dce94bc..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here -ALTER TABLE chat_table - ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; -ALTER TABLE chat_message_table - ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql deleted file mode 100644 index 50602eb129..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE user_workspace_table -DROP COLUMN auth_type; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql deleted file mode 100644 index 7d986e3e57..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Your SQL goes here -ALTER TABLE user_workspace_table - ADD COLUMN workspace_type INTEGER NOT NULL DEFAULT 1; - --- 2. Back‑fill from user_table.auth_type -UPDATE user_workspace_table -SET workspace_type = (SELECT ut.auth_type - FROM user_table ut - WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) -WHERE EXISTS (SELECT 1 - FROM user_table ut - WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)); - -ALTER TABLE user_table DROP COLUMN stability_ai_key; -ALTER TABLE user_table DROP COLUMN openai_key; -ALTER TABLE user_table DROP COLUMN workspace; -ALTER TABLE user_table DROP COLUMN encryption_type; -ALTER TABLE user_table DROP COLUMN ai_model; - -CREATE TABLE workspace_setting_table ( - id TEXT PRIMARY KEY NOT NULL , - disable_search_indexing BOOLEAN DEFAULT FALSE NOT NULL , - ai_model TEXT DEFAULT "" NOT NULL -); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f91d187b75..4ff70bf3c6 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -27,7 +27,6 @@ diesel::table! { author_id -> Text, reply_message_id -> Nullable, metadata -> Nullable, - is_sync -> Bool, } } @@ -36,9 +35,10 @@ diesel::table! { chat_id -> Text, created_at -> BigInt, name -> Text, + local_files -> Text, metadata -> Text, - rag_ids -> Nullable, - is_sync -> Bool, + local_enabled -> Bool, + sync_to_cloud -> Bool, } } @@ -89,11 +89,16 @@ diesel::table! { user_table (id) { id -> Text, name -> Text, + workspace -> Text, icon_url -> Text, + openai_key -> Text, token -> Text, email -> Text, auth_type -> Integer, + encryption_type -> Text, + stability_ai_key -> Text, updated_at -> BigInt, + ai_model -> Text, } } @@ -107,7 +112,6 @@ diesel::table! { icon -> Text, member_count -> BigInt, role -> Nullable, - workspace_type -> Integer, } } @@ -123,14 +127,6 @@ diesel::table! { } } -diesel::table! { - workspace_setting_table (id) { - id -> Text, - disable_search_indexing -> Bool, - ai_model -> Text, - } -} - diesel::allow_tables_to_appear_in_same_query!( af_collab_metadata, chat_local_setting_table, @@ -143,5 +139,4 @@ diesel::allow_tables_to_appear_in_same_query!( user_table, user_workspace_table, workspace_members_table, - workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-storage-pub/Cargo.toml b/frontend/rust-lib/flowy-storage-pub/Cargo.toml index d36c997432..ecab2212f8 100644 --- a/frontend/rust-lib/flowy-storage-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-storage-pub/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] lib-infra.workspace = true +serde_json.workspace = true serde.workspace = true async-trait.workspace = true mime = "0.3.17" @@ -16,4 +17,4 @@ mime_guess = "2.0.4" client-api-entity = { workspace = true } tokio = { workspace = true, features = ["sync", "io-util"] } anyhow = "1.0.86" -uuid.workspace = true \ No newline at end of file +tracing.workspace = true diff --git a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs index 5a72262ac9..6f12779899 100644 --- a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs @@ -3,7 +3,6 @@ use async_trait::async_trait; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use mime::Mime; -use uuid::Uuid; #[async_trait] pub trait StorageCloudService: Send + Sync { @@ -48,17 +47,17 @@ pub trait StorageCloudService: Send + Sync { async fn get_object(&self, url: String) -> Result; async fn get_object_url_v1( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, ) -> FlowyResult; /// Return workspace_id, parent_dir, file_id - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)>; + async fn parse_object_url_v1(&self, url: &str) -> Option<(String, String, String)>; async fn create_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, file_id: &str, content_type: &str, @@ -67,7 +66,7 @@ pub trait StorageCloudService: Send + Sync { async fn upload_part( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -77,7 +76,7 @@ pub trait StorageCloudService: Send + Sync { async fn complete_upload( &self, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -86,7 +85,7 @@ pub trait StorageCloudService: Send + Sync { } pub struct ObjectIdentity { - pub workspace_id: Uuid, + pub workspace_id: String, pub file_id: String, pub ext: String, } @@ -98,7 +97,7 @@ pub struct ObjectValue { } pub struct StorageObject { - pub workspace_id: Uuid, + pub workspace_id: String, pub file_name: String, pub value: ObjectValueSupabase, } @@ -127,9 +126,9 @@ impl StorageObject { /// * `name`: The name of the storage object. /// * `file_path`: The file path to the storage object's data. /// - pub fn from_file(workspace_id: &Uuid, file_name: &str, file_path: T) -> Self { + pub fn from_file(workspace_id: &str, file_name: &str, file_path: T) -> Self { Self { - workspace_id: *workspace_id, + workspace_id: workspace_id.to_string(), file_name: file_name.to_string(), value: ObjectValueSupabase::File { file_path: file_path.to_string(), @@ -146,14 +145,14 @@ impl StorageObject { /// * `mime`: The MIME type of the storage object. /// pub fn from_bytes>( - workspace_id: &Uuid, + workspace_id: &str, file_name: &str, bytes: B, mime: String, ) -> Self { let bytes = bytes.into(); Self { - workspace_id: *workspace_id, + workspace_id: workspace_id.to_string(), file_name: file_name.to_string(), value: ObjectValueSupabase::Bytes { bytes, mime }, } diff --git a/frontend/rust-lib/flowy-storage/Cargo.toml b/frontend/rust-lib/flowy-storage/Cargo.toml index add7996439..405faed1ba 100644 --- a/frontend/rust-lib/flowy-storage/Cargo.toml +++ b/frontend/rust-lib/flowy-storage/Cargo.toml @@ -17,6 +17,8 @@ tokio = { workspace = true, features = ["sync", "io-util"] } tracing.workspace = true flowy-sqlite.workspace = true mime_guess = "2.0.4" +fxhash = "0.2.1" +anyhow = "1.0.86" chrono = "0.4.33" flowy-notification = { workspace = true } flowy-derive.workspace = true @@ -24,8 +26,8 @@ protobuf = { workspace = true } dashmap.workspace = true strum_macros = "0.25.2" allo-isolate = { version = "^0.1", features = ["catch-unwind"] } +futures-util = "0.3.30" collab-importer = { workspace = true } -uuid.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["full"] } @@ -34,6 +36,7 @@ rand = { version = "0.8", features = ["std_rng"] } [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] [build-dependencies] flowy-codegen.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-storage/build.rs b/frontend/rust-lib/flowy-storage/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-storage/build.rs +++ b/frontend/rust-lib/flowy-storage/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs index 0dd729b087..66ad44e0fd 100644 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ b/frontend/rust-lib/flowy-storage/src/manager.rs @@ -24,17 +24,15 @@ use lib_infra::box_any::BoxAny; use lib_infra::isolate_stream::{IsolateSink, SinkExt}; use lib_infra::util::timestamp; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::atomic::AtomicBool; use std::sync::Arc; use tokio::io::AsyncWriteExt; use tokio::sync::{broadcast, watch}; use tracing::{debug, error, info, instrument, trace}; -use uuid::Uuid; pub trait StorageUserService: Send + Sync + 'static { fn user_id(&self) -> Result; - fn workspace_id(&self) -> Result; + fn workspace_id(&self) -> Result; fn sqlite_connection(&self, uid: i64) -> Result; fn get_application_root_dir(&self) -> &str; } @@ -159,8 +157,7 @@ impl StorageManager { let uid = self.user_service.user_id().ok()?; let mut conn = self.user_service.sqlite_connection(uid).ok()?; - let is_finish = - is_upload_completed(&mut conn, &workspace_id.to_string(), &parent_dir, &file_id).ok()?; + let is_finish = is_upload_completed(&mut conn, &workspace_id, &parent_dir, &file_id).ok()?; if let Err(err) = self.global_notifier.send(FileProgress::new_progress( url.to_string(), @@ -181,14 +178,6 @@ impl StorageManager { } } - pub async fn initialize_after_open_workspace(&self, workspace_id: &Uuid) { - self.enable_storage_write_access(); - - if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { - error!("prepare {} upload task failed: {}", workspace_id, err); - } - } - pub fn update_network_reachable(&self, reachable: bool) { if reachable { self.uploader.resume(); @@ -240,7 +229,7 @@ async fn prepare_upload_task( if let Ok(uid) = user_service.user_id() { let workspace_id = user_service.workspace_id()?; let conn = user_service.sqlite_connection(uid)?; - let upload_files = batch_select_upload_file(conn, &workspace_id.to_string(), 100, false)?; + let upload_files = batch_select_upload_file(conn, &workspace_id, 100, false)?; let tasks = upload_files .into_iter() .map(|upload_file| UploadTask::BackgroundTask { @@ -280,7 +269,7 @@ impl StorageService for StorageServiceImpl { self .task_queue - .remove_task(&workspace_id.to_string(), &parent_dir, &file_id) + .remove_task(&workspace_id, &parent_dir, &file_id) .await; trace!("[File] delete progress notifier: {}", file_id); @@ -289,7 +278,7 @@ impl StorageService for StorageServiceImpl { self .user_service .sqlite_connection(self.user_service.user_id()?)?, - &workspace_id.to_string(), + &workspace_id, &parent_dir, &file_id, ) { @@ -395,10 +384,9 @@ impl StorageService for StorageServiceImpl { let conn = self .user_service .sqlite_connection(self.user_service.user_id()?)?; - let workspace_id = Uuid::from_str(&record.workspace_id)?; let url = self .cloud_service - .get_object_url_v1(&workspace_id, &record.parent_dir, &record.file_id) + .get_object_url_v1(&record.workspace_id, &record.parent_dir, &record.file_id) .await?; let file_id = record.file_id.clone(); match insert_upload_file(conn, &record) { @@ -490,8 +478,7 @@ impl StorageService for StorageServiceImpl { .user_service .sqlite_connection(self.user_service.user_id()?)?; let workspace_id = self.user_service.workspace_id()?; - is_upload_completed(&mut conn, &workspace_id.to_string(), parent_idr, file_id) - .unwrap_or(false) + is_upload_completed(&mut conn, &workspace_id, parent_idr, file_id).unwrap_or(false) }; if is_completed { @@ -603,10 +590,9 @@ async fn start_upload( upload_file.file_id ); - let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; let create_upload_resp_result = cloud_service .create_upload( - &workspace_id, + &upload_file.workspace_id, &upload_file.parent_dir, &upload_file.file_id, &upload_file.content_type, @@ -615,7 +601,11 @@ async fn start_upload( .await; let file_url = cloud_service - .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) + .get_object_url_v1( + &upload_file.workspace_id, + &upload_file.parent_dir, + &upload_file.file_id, + ) .await?; if let Err(err) = create_upload_resp_result.as_ref() { @@ -663,7 +653,7 @@ async fn start_upload( match upload_part( cloud_service, user_service, - &workspace_id, + &upload_file.workspace_id, &upload_file.parent_dir, &upload_file.upload_id, &upload_file.file_id, @@ -792,7 +782,7 @@ async fn resume_upload( async fn upload_part( cloud_service: &Arc, user_service: &Arc, - workspace_id: &Uuid, + workspace_id: &str, parent_dir: &str, upload_id: &str, file_id: &str, @@ -832,9 +822,12 @@ async fn complete_upload( parts: Vec, global_notifier: &GlobalNotifier, ) -> Result<(), FlowyError> { - let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; let file_url = cloud_service - .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) + .get_object_url_v1( + &upload_file.workspace_id, + &upload_file.parent_dir, + &upload_file.file_id, + ) .await?; info!( @@ -845,7 +838,7 @@ async fn complete_upload( ); match cloud_service .complete_upload( - &workspace_id, + &upload_file.workspace_id, &upload_file.parent_dir, &upload_file.upload_id, &upload_file.file_id, diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index f8a673e918..0228e25d35 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -15,6 +15,7 @@ collab-entity = { workspace = true } serde_json.workspace = true serde_repr.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } +anyhow.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.14" flowy-folder-pub.workspace = true @@ -22,4 +23,3 @@ collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" client-api = { workspace = true } -flowy-sqlite.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index a99e8b8672..d68bf3f809 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -4,7 +4,6 @@ use client_api::entity::billing_dto::SubscriptionPlanDetail; pub use client_api::entity::billing_dto::SubscriptionStatus; use client_api::entity::billing_dto::WorkspaceSubscriptionStatus; use client_api::entity::billing_dto::WorkspaceUsageAndLimit; -use client_api::entity::GotrueTokenResponse; pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use collab_entity::{CollabObject, CollabType}; use flowy_error::{internal_error, ErrorCode, FlowyError}; @@ -20,8 +19,8 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, UserTokenState, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AuthResponse, Authenticator, Role, UpdateUserProfileParams, UserCredentials, UserProfile, + UserTokenState, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -84,13 +83,13 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - fn set_server_auth_type( - &self, - auth_type: &AuthType, - token: Option, - ) -> Result<(), FlowyError>; + /// Sets the authenticator when user sign in or sign up. + /// + /// # Arguments + /// * `authenticator`: An `Authenticator` object. + fn set_user_authenticator(&self, authenticator: &Authenticator); - fn get_server_auth_type(&self) -> AuthType; + fn get_user_authenticator(&self) -> Authenticator; /// Sets the network reachability /// @@ -136,7 +135,7 @@ pub trait UserCloudService: Send + Sync + 'static { /// Delete an account and all the data associated with the account async fn delete_account(&self) -> Result<(), FlowyError> { - Ok(()) + Err(FlowyError::not_support()) } /// Generate a sign in url for the user with the given email @@ -149,17 +148,11 @@ pub trait UserCloudService: Send + Sync + 'static { &self, email: &str, password: &str, - ) -> Result; + ) -> Result; async fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) -> Result<(), FlowyError>; - async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result; - /// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page. /// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.), /// which will call [Client::sign_in_with_url]generate_sign_in_url_with_email to sign in. @@ -168,14 +161,17 @@ pub trait UserCloudService: Send + Sync + 'static { async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result; /// Using the user's token to update the user information - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError>; + async fn update_user( + &self, + credential: UserCredentials, + params: UpdateUserProfileParams, + ) -> Result<(), FlowyError>; /// Get the user information using the user's token or uid /// return None if the user is not found - async fn get_user_profile(&self, uid: i64, workspace_id: &str) - -> Result; + async fn get_user_profile(&self, credential: UserCredentials) -> Result; - async fn open_workspace(&self, workspace_id: &Uuid) -> Result; + async fn open_workspace(&self, workspace_id: &str) -> Result; /// Return the all the workspaces of the user async fn get_all_workspace(&self, uid: i64) -> Result, FlowyError>; @@ -187,18 +183,18 @@ pub trait UserCloudService: Send + Sync + 'static { // Updates the workspace name and icon async fn patch_workspace( &self, - workspace_id: &Uuid, - new_workspace_name: Option, - new_workspace_icon: Option, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, ) -> Result<(), FlowyError>; /// Deletes a workspace owned by the user. - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; + async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError>; async fn invite_workspace_member( &self, invitee_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> Result<(), FlowyError> { Ok(()) @@ -218,7 +214,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, ) -> Result<(), FlowyError> { Ok(()) } @@ -226,7 +222,7 @@ pub trait UserCloudService: Send + Sync + 'static { async fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> Result<(), FlowyError> { Ok(()) @@ -234,14 +230,24 @@ pub trait UserCloudService: Send + Sync + 'static { async fn get_workspace_members( &self, - workspace_id: Uuid, - ) -> Result, FlowyError>; + workspace_id: String, + ) -> Result, FlowyError> { + Ok(vec![]) + } + + async fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> Result { + Err(FlowyError::not_support()) + } async fn get_user_awareness_doc_state( &self, uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, + workspace_id: &str, + object_id: &str, ) -> Result, FlowyError>; fn receive_realtime_event(&self, _json: Value) {} @@ -250,6 +256,8 @@ pub trait UserCloudService: Send + Sync + 'static { None } + async fn reset_workspace(&self, collab_object: CollabObject) -> Result<(), FlowyError>; + async fn create_collab_object( &self, collab_object: &CollabObject, @@ -258,17 +266,17 @@ pub trait UserCloudService: Send + Sync + 'static { async fn batch_create_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec, ) -> Result<(), FlowyError>; - async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + async fn leave_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> { Ok(()) } async fn subscribe_workspace( &self, - workspace_id: Uuid, + workspace_id: String, recurring_interval: RecurringInterval, workspace_subscription_plan: SubscriptionPlan, success_url: String, @@ -276,24 +284,27 @@ pub trait UserCloudService: Send + Sync + 'static { Err(FlowyError::not_support()) } - async fn get_workspace_member( + async fn get_workspace_member_info( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, - ) -> Result; + ) -> Result { + Err(FlowyError::not_support()) + } + /// Get all subscriptions for all workspaces for a user (email) async fn get_workspace_subscriptions( &self, ) -> Result, FlowyError> { - Ok(vec![]) + Err(FlowyError::not_support()) } /// Get the workspace subscriptions for a workspace async fn get_workspace_subscription_one( &self, - workspace_id: &Uuid, + workspace_id: String, ) -> Result, FlowyError> { - Ok(vec![]) + Err(FlowyError::not_support()) } async fn cancel_workspace_subscription( @@ -302,20 +313,22 @@ pub trait UserCloudService: Send + Sync + 'static { plan: SubscriptionPlan, reason: Option, ) -> Result<(), FlowyError> { - Ok(()) + Err(FlowyError::not_support()) } async fn get_workspace_plan( &self, - workspace_id: Uuid, + workspace_id: String, ) -> Result, FlowyError> { - Ok(vec![]) + Err(FlowyError::not_support()) } async fn get_workspace_usage( &self, - workspace_id: &Uuid, - ) -> Result; + workspace_id: String, + ) -> Result { + Err(FlowyError::not_support()) + } async fn get_billing_portal_url(&self) -> Result { Err(FlowyError::not_support()) @@ -323,27 +336,31 @@ pub trait UserCloudService: Send + Sync + 'static { async fn update_workspace_subscription_payment_period( &self, - workspace_id: &Uuid, + workspace_id: String, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> Result<(), FlowyError> { - Ok(()) + Err(FlowyError::not_support()) } async fn get_subscription_plan_details(&self) -> Result, FlowyError> { - Ok(vec![]) + Err(FlowyError::not_support()) } async fn get_workspace_setting( &self, - workspace_id: &Uuid, - ) -> Result; + workspace_id: &str, + ) -> Result { + Err(FlowyError::not_support()) + } async fn update_workspace_setting( &self, - workspace_id: &Uuid, + workspace_id: &str, workspace_settings: AFWorkspaceSettingsChange, - ) -> Result; + ) -> Result { + Err(FlowyError::not_support()) + } } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index a870b9c0b0..8f31edf740 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -1,15 +1,15 @@ -use std::fmt::{Display, Formatter}; use std::str::FromStr; use chrono::{DateTime, Utc}; pub use client_api::entity::billing_dto::RecurringInterval; use client_api::entity::AFRole; -use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_repr::*; use uuid::Uuid; +pub const USER_METADATA_OPEN_AI_KEY: &str = "openai_key"; +pub const USER_METADATA_STABILITY_AI_KEY: &str = "stability_ai_key"; pub const USER_METADATA_ICON_URL: &str = "icon_url"; pub const USER_METADATA_UPDATE_AT: &str = "updated_at"; @@ -31,7 +31,7 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, } #[derive(Serialize, Deserialize, Default, Debug)] @@ -39,7 +39,7 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, pub device_id: String, } @@ -100,6 +100,40 @@ impl UserAuthResponse for AuthResponse { } } +#[derive(Clone, Debug)] +pub struct UserCredentials { + /// Currently, the token is only used when the [Authenticator] is AppFlowyCloud + pub token: Option, + + /// The user id + pub uid: Option, + + /// The user id + pub uuid: Option, +} + +impl UserCredentials { + pub fn from_uid(uid: i64) -> Self { + Self { + token: None, + uid: Some(uid), + uuid: None, + } + } + + pub fn from_uuid(uuid: String) -> Self { + Self { + token: None, + uid: None, + uuid: Some(uuid), + } + } + + pub fn new(token: Option, uid: Option, uuid: Option) -> Self { + Self { token, uid, uuid } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserWorkspace { pub id: String, @@ -114,44 +148,37 @@ pub struct UserWorkspace { pub member_count: i64, #[serde(default)] pub role: Option, - #[serde(default = "default_workspace_type")] - pub workspace_type: AuthType, -} - -fn default_workspace_type() -> AuthType { - AuthType::AppFlowyCloud } impl UserWorkspace { - pub fn workspace_id(&self) -> FlowyResult { - let id = Uuid::from_str(&self.id)?; - Ok(id) - } - - pub fn new_local(workspace_id: String, name: &str) -> Self { + pub fn new_local(workspace_id: &str, _uid: i64) -> Self { Self { - id: workspace_id, - name: name.to_string(), + id: workspace_id.to_string(), + name: "".to_string(), created_at: Utc::now(), workspace_database_id: Uuid::new_v4().to_string(), icon: "".to_string(), member_count: 1, - role: Some(Role::Owner), - workspace_type: AuthType::Local, + role: None, } } } -#[derive(Default, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct UserProfile { + #[serde(rename = "id")] pub uid: i64, pub email: String, pub name: String, pub token: String, pub icon_url: String, - pub auth_type: AuthType, - pub workspace_auth_type: AuthType, + pub openai_key: String, + pub stability_ai_key: String, + pub authenticator: Authenticator, + // If the encryption_sign is not empty, which means the user has enabled the encryption. + pub encryption_type: EncryptionType, pub updated_at: i64, + pub ai_model: String, } #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] @@ -193,30 +220,43 @@ impl FromStr for EncryptionType { } } -impl From<(&T, &AuthType)> for UserProfile +impl From<(&T, &Authenticator)> for UserProfile where T: UserAuthResponse, { - fn from(params: (&T, &AuthType)) -> Self { + fn from(params: (&T, &Authenticator)) -> Self { let (value, auth_type) = params; - let icon_url = value - .metadata() - .as_ref() - .map(|m| { - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default() - }) - .unwrap_or_default(); + let (icon_url, openai_key, stability_ai_key) = { + value + .metadata() + .as_ref() + .map(|m| { + ( + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + m.get(USER_METADATA_OPEN_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + m.get(USER_METADATA_STABILITY_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + ) + }) + .unwrap_or_default() + }; Self { uid: value.user_id(), email: value.user_email().unwrap_or_default(), name: value.user_name().to_owned(), token: value.user_token().unwrap_or_default(), icon_url, - auth_type: *auth_type, - workspace_auth_type: *auth_type, + openai_key, + authenticator: auth_type.clone(), + encryption_type: value.encryption_type(), + stability_ai_key, updated_at: value.updated_at(), + ai_model: "".to_string(), } } } @@ -228,7 +268,11 @@ pub struct UpdateUserProfileParams { pub email: Option, pub password: Option, pub icon_url: Option, + pub openai_key: Option, + pub stability_ai_key: Option, + pub encryption_sign: Option, pub token: Option, + pub ai_model: Option, } impl UpdateUserProfileParams { @@ -263,11 +307,45 @@ impl UpdateUserProfileParams { self.icon_url = Some(icon_url.to_string()); self } + + pub fn with_openai_key(mut self, openai_key: &str) -> Self { + self.openai_key = Some(openai_key.to_owned()); + self + } + + pub fn with_stability_ai_key(mut self, stability_ai_key: &str) -> Self { + self.stability_ai_key = Some(stability_ai_key.to_owned()); + self + } + + pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self { + let sign = match encryption_type { + EncryptionType::NoEncryption => "".to_string(), + EncryptionType::SelfEncryption(sign) => sign, + }; + self.encryption_sign = Some(sign); + self + } + + pub fn with_ai_model(mut self, ai_model: &str) -> Self { + self.ai_model = Some(ai_model.to_owned()); + self + } + + pub fn is_empty(&self) -> bool { + self.name.is_none() + && self.email.is_none() + && self.password.is_none() + && self.icon_url.is_none() + && self.openai_key.is_none() + && self.encryption_sign.is_none() + && self.stability_ai_key.is_none() + } } -#[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] -pub enum AuthType { +pub enum Authenticator { /// It's a local server, we do fake sign in default. Local = 0, /// Currently not supported. It will be supported in the future when the @@ -275,37 +353,28 @@ pub enum AuthType { AppFlowyCloud = 1, } -impl Display for AuthType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AuthType::Local => write!(f, "Local"), - AuthType::AppFlowyCloud => write!(f, "AppFlowyCloud"), - } - } -} - -impl Default for AuthType { +impl Default for Authenticator { fn default() -> Self { Self::Local } } -impl AuthType { +impl Authenticator { pub fn is_local(&self) -> bool { - matches!(self, AuthType::Local) + matches!(self, Authenticator::Local) } pub fn is_appflowy_cloud(&self) -> bool { - matches!(self, AuthType::AppFlowyCloud) + matches!(self, Authenticator::AppFlowyCloud) } } -impl From for AuthType { +impl From for Authenticator { fn from(value: i32) -> Self { match value { - 0 => AuthType::Local, - 1 => AuthType::AppFlowyCloud, - _ => AuthType::Local, + 0 => Authenticator::Local, + 1 => Authenticator::AppFlowyCloud, + _ => Authenticator::Local, } } } @@ -326,7 +395,7 @@ pub enum UserTokenState { } // Workspace Role -#[derive(Clone, Copy, Debug, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum Role { Owner = 0, diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index 773ae96a9a..2e51ecc626 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,7 +1,6 @@ pub mod cloud; pub mod entities; pub mod session; -pub mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user-pub/src/session.rs b/frontend/rust-lib/flowy-user-pub/src/session.rs index 83a5670ddb..4c2668477a 100644 --- a/frontend/rust-lib/flowy-user-pub/src/session.rs +++ b/frontend/rust-lib/flowy-user-pub/src/session.rs @@ -1,4 +1,4 @@ -use crate::entities::{AuthType, UserAuthResponse, UserWorkspace}; +use crate::entities::{UserAuthResponse, UserWorkspace}; use base64::engine::general_purpose::STANDARD; use base64::Engine; use chrono::Utc; @@ -77,7 +77,6 @@ impl<'de> Visitor<'de> for SessionVisitor { icon: "".to_owned(), member_count: 1, role: None, - workspace_type: AuthType::Local, }) } } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs deleted file mode 100644 index 2a5f7bf891..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod member_sql; -mod user_sql; -mod workspace_setting_sql; -mod workspace_sql; - -pub use member_sql::*; -pub use user_sql::*; -pub use workspace_setting_sql::*; -pub use workspace_sql::*; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs deleted file mode 100644 index ca117300f2..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::cloud::UserUpdate; -use crate::entities::{AuthType, Role, UpdateUserProfileParams, UserProfile, UserWorkspace}; -use crate::sql::{ - select_user_workspace, upsert_user_workspace, upsert_workspace_member, WorkspaceMemberTable, -}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_table; -use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use tracing::trace; - -/// The order of the fields in the struct must be the same as the order of the fields in the table. -/// Check out the [schema.rs] for table schema. -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_table)] -pub struct UserTable { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) icon_url: String, - pub(crate) token: String, - pub(crate) email: String, - pub(crate) auth_type: i32, - pub(crate) updated_at: i64, -} - -#[allow(deprecated)] -impl From<(UserProfile, AuthType)> for UserTable { - fn from(value: (UserProfile, AuthType)) -> Self { - let (user_profile, auth_type) = value; - UserTable { - id: user_profile.uid.to_string(), - name: user_profile.name, - #[allow(deprecated)] - icon_url: user_profile.icon_url, - token: user_profile.token, - email: user_profile.email, - auth_type: auth_type as i32, - updated_at: user_profile.updated_at, - } - } -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_table)] -pub struct UserTableChangeset { - pub id: String, - pub name: Option, - pub email: Option, - pub icon_url: Option, - pub token: Option, -} - -impl UserTableChangeset { - pub fn new(params: UpdateUserProfileParams) -> Self { - UserTableChangeset { - id: params.uid.to_string(), - name: params.name, - email: params.email, - icon_url: params.icon_url, - token: params.token, - } - } - - pub fn from_user_profile(user_profile: UserProfile) -> Self { - UserTableChangeset { - id: user_profile.uid.to_string(), - name: Some(user_profile.name), - email: Some(user_profile.email), - icon_url: Some(user_profile.icon_url), - token: Some(user_profile.token), - } - } -} - -impl From for UserTableChangeset { - fn from(value: UserUpdate) -> Self { - UserTableChangeset { - id: value.uid.to_string(), - name: value.name, - email: value.email, - ..Default::default() - } - } -} - -pub fn update_user_profile( - conn: &mut SqliteConnection, - changeset: UserTableChangeset, -) -> Result<(), FlowyError> { - trace!("update user profile: {:?}", changeset); - let user_id = changeset.id.clone(); - update(user_table::dsl::user_table.filter(user_table::id.eq(&user_id))) - .set(changeset) - .execute(conn)?; - Ok(()) -} - -pub fn insert_local_workspace( - uid: i64, - workspace_id: &str, - workspace_name: &str, - conn: &mut SqliteConnection, -) -> FlowyResult { - let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), workspace_name); - conn.immediate_transaction(|conn| { - let row = select_user_table_row(uid, conn)?; - let row = WorkspaceMemberTable { - email: row.email, - role: Role::Owner as i32, - name: row.name, - avatar_url: Some(row.icon_url), - uid, - workspace_id: workspace_id.to_string(), - updated_at: chrono::Utc::now().naive_utc(), - }; - - upsert_user_workspace(uid, AuthType::Local, user_workspace.clone(), conn)?; - upsert_workspace_member(conn, row)?; - Ok::<_, FlowyError>(()) - })?; - - Ok(user_workspace) -} - -fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result { - let row = user_table::dsl::user_table - .filter(user_table::id.eq(&uid.to_string())) - .first::(conn) - .map_err(|err| { - FlowyError::record_not_found().with_context(format!( - "Can't find the user profile for user id: {}, error: {:?}", - uid, err - )) - })?; - Ok(row) -} - -pub fn select_user_profile( - uid: i64, - workspace_id: &str, - conn: &mut SqliteConnection, -) -> Result { - let workspace = select_user_workspace(workspace_id, conn)?; - let workspace_auth_type = AuthType::from(workspace.workspace_type); - let row = select_user_table_row(uid, conn)?; - - let user = UserProfile { - uid: row.id.parse::().unwrap_or(0), - email: row.email, - name: row.name, - token: row.token, - icon_url: row.icon_url, - auth_type: AuthType::from(row.auth_type), - workspace_auth_type, - updated_at: row.updated_at, - }; - - Ok(user) -} - -pub fn select_user_auth_type( - uid: i64, - conn: &mut SqliteConnection, -) -> Result { - let row = select_user_table_row(uid, conn)?; - Ok(AuthType::from(row.auth_type)) -} - -pub fn select_user_token(uid: i64, conn: &mut SqliteConnection) -> Result { - let row = select_user_table_row(uid, conn)?; - Ok(row.token) -} - -pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - // delete old user if exists - diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) - .execute(conn)?; - - let _ = diesel::insert_into(user_table::table) - .values(user) - .execute(conn)?; - Ok::<(), FlowyError>(()) - })?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs deleted file mode 100644 index 7eeafaf1e4..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs +++ /dev/null @@ -1,72 +0,0 @@ -use client_api::entity::AFWorkspaceSettings; -use flowy_error::FlowyError; -use flowy_sqlite::schema::workspace_setting_table; -use flowy_sqlite::schema::workspace_setting_table::dsl; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{prelude::*, ExpressionMethods}; -use uuid::Uuid; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = workspace_setting_table)] -pub struct WorkspaceSettingsTable { - pub id: String, - pub disable_search_indexing: bool, - pub ai_model: String, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = workspace_setting_table)] -pub struct WorkspaceSettingsChangeset { - pub id: String, - pub disable_search_indexing: Option, - pub ai_model: Option, -} - -impl WorkspaceSettingsTable { - pub fn from_workspace_settings(workspace_id: &Uuid, settings: &AFWorkspaceSettings) -> Self { - Self { - id: workspace_id.to_string(), - disable_search_indexing: settings.disable_search_indexing, - ai_model: settings.ai_model.clone(), - } - } -} - -pub fn update_workspace_setting( - conn: &mut DBConnection, - changeset: WorkspaceSettingsChangeset, -) -> Result<(), FlowyError> { - diesel::update(dsl::workspace_setting_table) - .filter(workspace_setting_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(conn)?; - Ok(()) -} - -/// Upserts a workspace setting into the database. -pub fn upsert_workspace_setting( - conn: &mut SqliteConnection, - settings: WorkspaceSettingsTable, -) -> Result<(), FlowyError> { - diesel::insert_into(dsl::workspace_setting_table) - .values(settings.clone()) - .on_conflict(workspace_setting_table::id) - .do_update() - .set(( - workspace_setting_table::disable_search_indexing.eq(settings.disable_search_indexing), - workspace_setting_table::ai_model.eq(settings.ai_model), - )) - .execute(conn)?; - Ok(()) -} - -/// Selects a workspace setting by id from the database. -pub fn select_workspace_setting( - conn: &mut SqliteConnection, - workspace_id: &str, -) -> Result { - let setting = dsl::workspace_setting_table - .filter(workspace_setting_table::id.eq(workspace_id)) - .first::(conn)?; - Ok(setting) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs deleted file mode 100644 index 27f63e3de6..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::entities::{AuthType, UserWorkspace}; -use chrono::{TimeZone, Utc}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::schema::user_workspace_table::dsl; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{prelude::*, ExpressionMethods, RunQueryDsl, SqliteConnection}; -use std::collections::{HashMap, HashSet}; -use tracing::{info, warn}; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceTable { - pub id: String, - pub name: String, - pub uid: i64, - pub created_at: i64, - pub database_storage_id: String, - pub icon: String, - pub member_count: i64, - pub role: Option, - pub workspace_type: i32, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceChangeset { - pub id: String, - pub name: Option, - pub icon: Option, - pub role: Option, - pub member_count: Option, -} - -impl UserWorkspaceChangeset { - pub fn has_changes(&self) -> bool { - self.name.is_some() || self.icon.is_some() || self.role.is_some() || self.member_count.is_some() - } - pub fn from_version(old: &UserWorkspace, new: &UserWorkspace) -> Self { - let mut changeset = Self { - id: new.id.clone(), - name: None, - icon: None, - role: None, - member_count: None, - }; - - if old.name != new.name { - changeset.name = Some(new.name.clone()); - } - if old.icon != new.icon { - changeset.icon = Some(new.icon.clone()); - } - if old.role != new.role { - changeset.role = new.role.map(|v| v as i32); - } - if old.member_count != new.member_count { - changeset.member_count = Some(new.member_count); - } - - changeset - } -} - -impl UserWorkspaceTable { - pub fn from_workspace( - uid_val: i64, - workspace: &UserWorkspace, - auth_type: AuthType, - ) -> Result { - if workspace.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if workspace.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: workspace.id.clone(), - name: workspace.name.clone(), - uid: uid_val, - created_at: workspace.created_at.timestamp(), - database_storage_id: workspace.workspace_database_id.clone(), - icon: workspace.icon.clone(), - member_count: workspace.member_count, - role: workspace.role.map(|v| v as i32), - workspace_type: auth_type as i32, - }) - } -} - -pub fn select_user_workspace( - workspace_id: &str, - conn: &mut SqliteConnection, -) -> FlowyResult { - let row = dsl::user_workspace_table - .filter(user_workspace_table::id.eq(workspace_id)) - .first::(conn)?; - Ok(row) -} - -pub fn select_all_user_workspace( - uid: i64, - conn: &mut SqliteConnection, -) -> Result, FlowyError> { - let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .order(user_workspace_table::created_at.desc()) - .load::(conn)?; - Ok(rows.into_iter().map(UserWorkspace::from).collect()) -} - -pub fn update_user_workspace( - mut conn: DBConnection, - changeset: UserWorkspaceChangeset, -) -> Result<(), FlowyError> { - diesel::update(user_workspace_table::dsl::user_workspace_table) - .filter(user_workspace_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(&mut conn)?; - - Ok(()) -} - -pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::(rows_affected) - })?; - - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - -impl From for UserWorkspace { - fn from(value: UserWorkspaceTable) -> Self { - Self { - id: value.id, - name: value.name, - created_at: Utc - .timestamp_opt(value.created_at, 0) - .single() - .unwrap_or_default(), - workspace_database_id: value.database_storage_id, - icon: value.icon, - member_count: value.member_count, - role: value.role.map(|v| v.into()), - workspace_type: AuthType::from(value.workspace_type), - } - } -} - -/// Delete all user workspaces for the given user and auth type. -pub fn delete_user_all_workspace( - uid: i64, - auth_type: AuthType, - conn: &mut SqliteConnection, -) -> FlowyResult<()> { - let n = diesel::delete( - dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::workspace_type.eq(auth_type as i32)), - ) - .execute(conn)?; - info!( - "Delete {} workspaces for user {} and auth type {:?}", - n, uid, auth_type - ); - Ok(()) -} - -#[derive(Debug)] -pub enum WorkspaceChange { - Inserted(String), - Updated(String), -} - -pub fn upsert_user_workspace( - uid_val: i64, - auth_type: AuthType, - user_workspace: UserWorkspace, - conn: &mut SqliteConnection, -) -> Result { - let row = UserWorkspaceTable::from_workspace(uid_val, &user_workspace, auth_type)?; - let n = insert_into(user_workspace_table::table) - .values(row.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(row.name), - user_workspace_table::uid.eq(row.uid), - user_workspace_table::created_at.eq(row.created_at), - user_workspace_table::database_storage_id.eq(row.database_storage_id), - user_workspace_table::icon.eq(row.icon), - user_workspace_table::member_count.eq(row.member_count), - user_workspace_table::role.eq(row.role), - )) - .execute(conn)?; - - Ok(n) -} - -pub fn sync_user_workspaces_with_diff( - uid_val: i64, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], - conn: &mut SqliteConnection, -) -> FlowyResult> { - let diff = conn.immediate_transaction(|conn| { - // 1) Load all existing workspaces into a map - let existing_rows: Vec = dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid_val)) - .filter(user_workspace_table::workspace_type.eq(auth_type as i32)) - .load(conn)?; - let mut existing_map: HashMap = existing_rows - .into_iter() - .map(|r| (r.id.clone(), r)) - .collect(); - - // 2) Build incoming ID set and delete any stale ones - let incoming_ids: HashSet = user_workspaces.iter().map(|uw| uw.id.clone()).collect(); - let to_delete: Vec = existing_map - .keys() - .filter(|id| !incoming_ids.contains(*id)) - .cloned() - .collect(); - - if !to_delete.is_empty() { - diesel::delete(dsl::user_workspace_table.filter(user_workspace_table::id.eq_any(&to_delete))) - .execute(conn)?; - } - - // 3) For each incoming workspace, either INSERT or UPDATE if changed - let mut diffs = Vec::new(); - for uw in user_workspaces { - match existing_map.remove(&uw.id) { - None => { - // new workspace → insert - let new_row = UserWorkspaceTable::from_workspace(uid_val, uw, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_row) - .execute(conn)?; - diffs.push(WorkspaceChange::Inserted(uw.id.clone())); - }, - - Some(old) => { - let changes = UserWorkspaceChangeset::from_version(&UserWorkspace::from(old), uw); - if changes.has_changes() { - diesel::update(dsl::user_workspace_table.find(&uw.id)) - .set(&changes) - .execute(conn)?; - diffs.push(WorkspaceChange::Updated(uw.id.clone())); - } - }, - } - } - - Ok::<_, FlowyError>(diffs) - })?; - Ok(diff) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs index 84185d310f..2b1b58ae05 100644 --- a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs +++ b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs @@ -3,7 +3,6 @@ use flowy_error::FlowyResult; use flowy_folder_pub::entities::ImportFrom; use lib_infra::async_trait::async_trait; use std::collections::HashMap; -use uuid::Uuid; #[async_trait] pub trait UserWorkspaceService: Send + Sync { @@ -20,5 +19,5 @@ pub trait UserWorkspaceService: Send + Sync { ) -> FlowyResult<()>; /// Removes local indexes when a workspace is left/deleted - async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>; + fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 65be4cc3f9..2a02043f38 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -31,9 +31,12 @@ tracing.workspace = true bytes.workspace = true serde = { workspace = true, features = ["rc"] } serde_json.workspace = true +serde_repr.workspace = true protobuf.workspace = true lazy_static = "1.4.0" diesel.workspace = true +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +once_cell = "1.17.1" strum = "0.25" strum_macros = "0.25.2" tokio = { workspace = true, features = ["rt"] } @@ -48,6 +51,7 @@ validator = { workspace = true, features = ["derive"] } rayon = "1.10.0" [dev-dependencies] +nanoid = "0.4.0" fake = "2.0.0" rand = "0.8.4" quickcheck = "1.0.3" @@ -56,6 +60,7 @@ quickcheck_macros = "1.0" [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-user/build.rs b/frontend/rust-lib/flowy-user/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-user/build.rs +++ b/frontend/rust-lib/flowy-user/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index a61ba5cc96..dbfd9b811a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,13 +1,12 @@ use std::collections::HashMap; use std::convert::TryInto; -use crate::entities::parser::*; -use crate::entities::AuthTypePB; -use crate::errors::ErrorCode; -use client_api::entity::GotrueTokenResponse; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; +use crate::entities::parser::*; +use crate::errors::ErrorCode; + #[derive(ProtoBuf, Default)] pub struct SignInPayloadPB { #[pb(index = 1)] @@ -20,7 +19,7 @@ pub struct SignInPayloadPB { pub name: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -31,10 +30,11 @@ impl TryInto for SignInPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; + let password = UserPassword::parse(self.password)?; Ok(SignInParams { email: email.0, - password: self.password, + password: password.0, name: self.name, auth_type: self.auth_type.into(), }) @@ -53,7 +53,7 @@ pub struct SignUpPayloadPB { pub password: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -64,13 +64,13 @@ impl TryInto for SignUpPayloadPB { fn try_into(self) -> Result { let email = UserEmail::parse(self.email)?; - let password = self.password; + let password = UserPassword::parse(self.password)?; let name = UserName::parse(self.name)?; Ok(SignUpParams { email: email.0, name: name.0, - password, + password: password.0, auth_type: self.auth_type.into(), device_id: self.device_id, }) @@ -86,53 +86,6 @@ pub struct MagicLinkSignInPB { pub redirect_to: String, } -#[derive(ProtoBuf, Default)] -pub struct PasscodeSignInPB { - #[pb(index = 1)] - pub email: String, - - #[pb(index = 2)] - pub passcode: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct GotrueTokenResponsePB { - #[pb(index = 1)] - pub access_token: String, - - #[pb(index = 2)] - pub token_type: String, - - #[pb(index = 3)] - pub expires_in: i64, - - #[pb(index = 4)] - pub expires_at: i64, - - #[pb(index = 5)] - pub refresh_token: String, - - #[pb(index = 6, one_of)] - pub provider_access_token: Option, - - #[pb(index = 7, one_of)] - pub provider_refresh_token: Option, -} - -impl From for GotrueTokenResponsePB { - fn from(response: GotrueTokenResponse) -> Self { - Self { - access_token: response.access_token, - token_type: response.token_type, - expires_in: response.expires_in, - expires_at: response.expires_at, - refresh_token: response.refresh_token, - provider_access_token: response.provider_access_token, - provider_refresh_token: response.provider_refresh_token, - } - } -} - #[derive(ProtoBuf, Default)] pub struct OauthSignInPB { /// Use this field to store the third party auth information. @@ -144,7 +97,7 @@ pub struct OauthSignInPB { pub map: HashMap, #[pb(index = 2)] - pub authenticator: AuthTypePB, + pub authenticator: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -153,7 +106,7 @@ pub struct SignInUrlPayloadPB { pub email: String, #[pb(index = 2)] - pub authenticator: AuthTypePB, + pub authenticator: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -228,10 +181,85 @@ pub struct OauthProviderDataPB { pub oauth_url: String, } +#[repr(u8)] +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum AuthenticatorPB { + Local = 0, + AppFlowyCloud = 2, +} + +impl From for AuthenticatorPB { + fn from(auth_type: Authenticator) -> Self { + match auth_type { + Authenticator::Local => AuthenticatorPB::Local, + Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, + } + } +} + +impl From for Authenticator { + fn from(pb: AuthenticatorPB) -> Self { + match pb { + AuthenticatorPB::Local => Authenticator::Local, + AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, + } + } +} + +impl Default for AuthenticatorPB { + fn default() -> Self { + Self::Local + } +} + +#[derive(Debug, ProtoBuf, Default)] +pub struct UserCredentialsPB { + #[pb(index = 1, one_of)] + pub uid: Option, + + #[pb(index = 2, one_of)] + pub uuid: Option, + + #[pb(index = 3, one_of)] + pub token: Option, +} + +impl UserCredentialsPB { + pub fn from_uid(uid: i64) -> Self { + Self { + uid: Some(uid), + uuid: None, + token: None, + } + } + + pub fn from_token(token: &str) -> Self { + Self { + uid: None, + uuid: None, + token: Some(token.to_owned()), + } + } + + pub fn from_uuid(uuid: &str) -> Self { + Self { + uid: None, + uuid: Some(uuid.to_owned()), + token: None, + } + } +} + +impl From for UserCredentials { + fn from(value: UserCredentialsPB) -> Self { + Self::new(value.token, value.uid, value.uuid) + } +} + #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 77d92fb33a..aa9d38a9cd 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,14 +1,16 @@ -use super::AFRolePB; -use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::AuthTypePB; -use crate::errors::ErrorCode; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::UserWorkspaceTable; use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; +use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; +use crate::entities::AuthenticatorPB; +use crate::errors::ErrorCode; + +use super::parser::UserStabilityAIKey; +use super::AFRolePB; + #[derive(Default, ProtoBuf)] pub struct UserTokenPB { #[pb(index = 1)] @@ -39,10 +41,22 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub user_auth_type: AuthTypePB, + pub openai_key: String, #[pb(index = 7)] - pub workspace_auth_type: AuthTypePB, + pub authenticator: AuthenticatorPB, + + #[pb(index = 8)] + pub encryption_sign: String, + + #[pb(index = 9)] + pub encryption_type: EncryptionTypePB, + + #[pb(index = 10)] + pub stability_ai_key: String, + + #[pb(index = 11)] + pub ai_model: String, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -59,14 +73,26 @@ impl Default for EncryptionTypePB { impl From for UserProfilePB { fn from(user_profile: UserProfile) -> Self { + let (encryption_sign, encryption_ty) = match user_profile.encryption_type { + EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), + EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), + }; + let mut ai_model = user_profile.ai_model; + if ai_model.is_empty() { + ai_model = "Default".to_string(); + } Self { id: user_profile.uid, email: user_profile.email, name: user_profile.name, token: user_profile.token, icon_url: user_profile.icon_url, - user_auth_type: user_profile.auth_type.into(), - workspace_auth_type: user_profile.workspace_auth_type.into(), + openai_key: user_profile.openai_key, + authenticator: user_profile.authenticator.into(), + encryption_sign, + encryption_type: encryption_ty, + stability_ai_key: user_profile.stability_ai_key, + ai_model, } } } @@ -87,6 +113,12 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 5, one_of)] pub icon_url: Option, + + #[pb(index = 6, one_of)] + pub openai_key: Option, + + #[pb(index = 7, one_of)] + pub stability_ai_key: Option, } impl UpdateUserProfilePayloadPB { @@ -116,6 +148,16 @@ impl UpdateUserProfilePayloadPB { self.icon_url = Some(icon_url.to_owned()); self } + + pub fn openai_key(mut self, openai_key: &str) -> Self { + self.openai_key = Some(openai_key.to_owned()); + self + } + + pub fn stability_ai_key(mut self, stability_ai_key: &str) -> Self { + self.stability_ai_key = Some(stability_ai_key.to_owned()); + self + } } impl TryInto for UpdateUserProfilePayloadPB { @@ -132,20 +174,37 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(email) => Some(UserEmail::parse(email)?.0), }; - let password = self.password; + let password = match self.password { + None => None, + Some(password) => Some(UserPassword::parse(password)?.0), + }; let icon_url = match self.icon_url { None => None, Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), }; + let openai_key = match self.openai_key { + None => None, + Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0), + }; + + let stability_ai_key = match self.stability_ai_key { + None => None, + Some(stability_ai_key) => Some(UserStabilityAIKey::parse(stability_ai_key)?.0), + }; + Ok(UpdateUserProfileParams { uid: self.id, name, email, password, icon_url, + openai_key, + encryption_sign: None, token: None, + stability_ai_key, + ai_model: None, }) } } @@ -184,35 +243,17 @@ pub struct UserWorkspacePB { #[pb(index = 6, one_of)] pub role: Option, - - #[pb(index = 7)] - pub workspace_auth_type: AuthTypePB, } impl From for UserWorkspacePB { - fn from(workspace: UserWorkspace) -> Self { - Self { - workspace_id: workspace.id, - name: workspace.name, - created_at_timestamp: workspace.created_at.timestamp(), - icon: workspace.icon, - member_count: workspace.member_count, - role: workspace.role.map(AFRolePB::from), - workspace_auth_type: AuthTypePB::from(workspace.workspace_type), - } - } -} - -impl From for UserWorkspacePB { - fn from(value: UserWorkspaceTable) -> Self { + fn from(value: UserWorkspace) -> Self { Self { workspace_id: value.id, name: value.name, - created_at_timestamp: value.created_at, + created_at_timestamp: value.created_at.timestamp(), icon: value.icon, member_count: value.member_count, role: value.role.map(AFRolePB::from), - workspace_auth_type: AuthTypePB::from(value.workspace_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 860bda3be7..885ad6f3cf 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -7,8 +7,7 @@ use validator::Validate; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; -use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; -use flowy_user_pub::sql::WorkspaceSettingsTable; +use flowy_user_pub::entities::{Role, WorkspaceInvitation, WorkspaceMember}; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] @@ -147,7 +146,7 @@ pub struct UpdateWorkspaceMemberPB { } // Workspace Role -#[derive(Debug, ProtoBuf_Enum, Clone, Default, Eq, PartialEq)] +#[derive(Debug, ProtoBuf_Enum, Clone, Default)] pub enum AFRolePB { Owner = 0, Member = 1, @@ -155,17 +154,6 @@ pub enum AFRolePB { Guest = 2, } -impl From for AFRolePB { - fn from(value: i32) -> Self { - match value { - 0 => AFRolePB::Owner, - 1 => AFRolePB::Member, - 2 => AFRolePB::Guest, - _ => AFRolePB::Guest, - } - } -} - impl From for Role { fn from(value: AFRolePB) -> Self { match value { @@ -193,16 +181,6 @@ pub struct UserWorkspaceIdPB { pub workspace_id: String, } -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct OpenUserWorkspacePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - - #[pb(index = 2)] - pub workspace_auth_type: AuthTypePB, -} - #[derive(ProtoBuf, Default, Clone, Validate)] pub struct CancelWorkspaceSubscriptionPB { #[pb(index = 1)] @@ -237,45 +215,6 @@ pub struct CreateWorkspacePB { #[pb(index = 1)] #[validate(custom(function = "required_not_empty_str"))] pub name: String, - - #[pb(index = 2)] - pub auth_type: AuthTypePB, -} - -#[derive(ProtoBuf_Enum, Copy, Default, Debug, Clone, Eq, PartialEq)] -#[repr(u8)] -pub enum AuthTypePB { - #[default] - Local = 0, - Server = 1, -} - -impl From for AuthTypePB { - fn from(value: i32) -> Self { - match value { - 0 => AuthTypePB::Local, - 1 => AuthTypePB::Server, - _ => AuthTypePB::Server, - } - } -} - -impl From for AuthTypePB { - fn from(value: AuthType) -> Self { - match value { - AuthType::Local => AuthTypePB::Local, - AuthType::AppFlowyCloud => AuthTypePB::Server, - } - } -} - -impl From for AuthType { - fn from(value: AuthTypePB) -> Self { - match value { - AuthTypePB::Local => AuthType::Local, - AuthTypePB::Server => AuthType::AppFlowyCloud, - } - } } #[derive(ProtoBuf, Default, Clone, Validate)] @@ -436,8 +375,8 @@ pub struct BillingPortalPB { pub url: String, } -#[derive(ProtoBuf, Default, Clone, Validate, Eq, PartialEq)] -pub struct WorkspaceSettingsPB { +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct UseAISettingPB { #[pb(index = 1)] pub disable_search_indexing: bool, @@ -445,17 +384,8 @@ pub struct WorkspaceSettingsPB { pub ai_model: String, } -impl From<&AFWorkspaceSettings> for WorkspaceSettingsPB { - fn from(value: &AFWorkspaceSettings) -> Self { - Self { - disable_search_indexing: value.disable_search_indexing, - ai_model: value.ai_model.clone(), - } - } -} - -impl From for WorkspaceSettingsPB { - fn from(value: WorkspaceSettingsTable) -> Self { +impl From for UseAISettingPB { + fn from(value: AFWorkspaceSettings) -> Self { Self { disable_search_indexing: value.disable_search_indexing, ai_model: value.ai_model, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index cbcf6f4477..c22af4f1c1 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,3 +1,14 @@ +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_sqlite::kv::KVStorePreferences; +use flowy_user_pub::cloud::UserCloudConfig; +use flowy_user_pub::entities::*; +use lib_dispatch::prelude::*; +use lib_infra::box_any::BoxAny; +use serde_json::Value; +use std::sync::Weak; +use std::{convert::TryInto, sync::Arc}; +use tracing::{event, trace}; + use crate::entities::*; use crate::notification::{send_notification, UserNotification}; use crate::services::cloud_config::{ @@ -5,18 +16,6 @@ use crate::services::cloud_config::{ }; use crate::services::data_import::prepare_import; use crate::user_manager::UserManager; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::entities::*; -use flowy_user_pub::sql::UserWorkspaceChangeset; -use lib_dispatch::prelude::*; -use lib_infra::box_any::BoxAny; -use serde_json::Value; -use std::str::FromStr; -use std::sync::Weak; -use std::{convert::TryInto, sync::Arc}; -use tracing::{event, trace}; -use uuid::Uuid; fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { let manager = manager @@ -40,16 +39,20 @@ fn upgrade_store_preferences( pub async fn sign_in_with_email_password_handler( data: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; + let auth_type = params.auth_type.clone(); - match manager - .sign_in_with_password(¶ms.email, ¶ms.password) - .await - { - Ok(token) => data_result_ok(token.into()), - Err(err) => Err(err), + let old_authenticator = manager.cloud_services.get_user_authenticator(); + match manager.sign_in(params, auth_type).await { + Ok(profile) => data_result_ok(UserProfilePB::from(profile)), + Err(err) => { + manager + .cloud_services + .set_user_authenticator(&old_authenticator); + return Err(err); + }, } } @@ -69,11 +72,17 @@ pub async fn sign_up( ) -> DataResult { let manager = upgrade_manager(manager)?; let params: SignUpParams = data.into_inner().try_into()?; - let auth_type = params.auth_type; + let authenticator = params.auth_type.clone(); - match manager.sign_up(auth_type, BoxAny::new(params)).await { + let old_authenticator = manager.cloud_services.get_user_authenticator(); + match manager.sign_up(authenticator, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), - Err(err) => Err(err), + Err(err) => { + manager + .cloud_services + .set_user_authenticator(&old_authenticator); + return Err(err); + }, } } @@ -91,28 +100,22 @@ pub async fn get_user_profile_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let session = manager.get_session()?; - - let mut user_profile = manager - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; + let uid = manager.get_session()?.user_id; + let mut user_profile = manager.get_user_profile_from_disk(uid).await?; let weak_manager = Arc::downgrade(&manager); let cloned_user_profile = user_profile.clone(); - let workspace_id = session.user_workspace.id.clone(); // Refresh the user profile in the background tokio::spawn(async move { if let Some(manager) = weak_manager.upgrade() { - let _ = manager - .refresh_user_profile(&cloned_user_profile, &workspace_id) - .await; + let _ = manager.refresh_user_profile(&cloned_user_profile).await; } }); // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.auth_type == AuthType::Local { + if user_profile.authenticator == Authenticator::Local { user_profile.email = "".to_string(); } @@ -314,19 +317,6 @@ pub async fn sign_in_with_magic_link_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip(data, manager), err)] -pub async fn sign_in_with_passcode_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let params = data.into_inner(); - let response = manager - .sign_in_with_passcode(¶ms.email, ¶ms.passcode) - .await?; - data_result_ok(response.into()) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn oauth_sign_in_handler( data: AFPluginData, @@ -334,7 +324,7 @@ pub async fn oauth_sign_in_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: AuthType = params.authenticator.into(); + let authenticator: Authenticator = params.authenticator.into(); let user_profile = manager .sign_up(authenticator, BoxAny::new(params.map)) .await?; @@ -348,7 +338,7 @@ pub async fn gen_sign_in_url_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: AuthType = params.authenticator.into(); + let authenticator: Authenticator = params.authenticator.into(); let sign_in_url = manager .generate_sign_in_url_with_email(&authenticator, ¶ms.email) .await?; @@ -369,6 +359,66 @@ pub async fn sign_in_with_provider_handler( }) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn set_encrypt_secret_handler( + manager: AFPluginState>, + data: AFPluginData, + store_preferences: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let store_preferences = upgrade_store_preferences(store_preferences)?; + let data = data.into_inner(); + match data.encryption_type { + EncryptionTypePB::NoEncryption => { + tracing::error!("Encryption type is NoEncryption, but set encrypt secret"); + }, + EncryptionTypePB::Symmetric => { + manager.check_encryption_sign_with_secret( + data.user_id, + &data.encryption_sign, + &data.encryption_secret, + )?; + + let config = UserCloudConfig::new(data.encryption_secret).with_enable_encrypt(true); + manager + .set_encrypt_secret( + data.user_id, + config.encrypt_secret.clone(), + EncryptionType::SelfEncryption(data.encryption_sign), + ) + .await?; + save_cloud_config(data.user_id, &store_preferences, &config)?; + }, + } + + manager.resume_sign_up().await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn check_encrypt_secret_handler( + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let uid = manager.get_session()?.user_id; + let profile = manager.get_user_profile_from_disk(uid).await?; + + let is_need_secret = match profile.encryption_type { + EncryptionType::NoEncryption => false, + EncryptionType::SelfEncryption(sign) => { + if sign.is_empty() { + false + } else { + manager.check_encryption_sign(uid, &sign).is_err() + } + }, + }; + + data_result_ok(UserEncryptionConfigurationPB { + require_secret: is_need_secret, + }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_cloud_config_handler( manager: AFPluginState>, @@ -384,18 +434,40 @@ pub async fn set_cloud_config_handler( if let Some(enable_sync) = update.enable_sync { manager - .cloud_service + .cloud_services .set_enable_sync(session.user_id, enable_sync); config.enable_sync = enable_sync; } + if let Some(enable_encrypt) = update.enable_encrypt { + debug_assert!(enable_encrypt, "Disable encryption is not supported"); + + if enable_encrypt { + tracing::info!("Enable encryption for user: {}", session.user_id); + config = config.with_enable_encrypt(enable_encrypt); + let encrypt_secret = config.encrypt_secret.clone(); + + // The encryption secret is generated when the user first enables encryption and will be + // used to validate the encryption secret is correct when the user logs in. + let encryption_sign = manager.generate_encryption_sign(session.user_id, &encrypt_secret)?; + let encryption_type = EncryptionType::SelfEncryption(encryption_sign); + manager + .set_encrypt_secret(session.user_id, encrypt_secret, encryption_type.clone()) + .await?; + + let params = + UpdateUserProfileParams::new(session.user_id).with_encryption_type(encryption_type); + manager.update_user_profile(params).await?; + } + } + save_cloud_config(session.user_id, &store_preferences, &config)?; let payload = CloudSettingPB { enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_service.service_url(), + server_url: manager.cloud_services.service_url(), }; send_notification( @@ -422,7 +494,7 @@ pub async fn get_cloud_config_handler( enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_service.service_url(), + server_url: manager.cloud_services.service_url(), }) } @@ -431,44 +503,22 @@ pub async fn get_all_workspace_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let session = manager.get_session()?; - let profile = manager - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; - let user_workspaces = manager - .get_all_user_workspaces(profile.uid, profile.auth_type) - .await?; - - data_result_ok(RepeatedUserWorkspacePB::from(user_workspaces)) + let uid = manager.get_session()?.user_id; + let user_workspaces = manager.get_all_user_workspaces(uid).await?; + data_result_ok(user_workspaces.into()) } #[tracing::instrument(level = "info", skip(data, manager), err)] pub async fn open_workspace_handler( - data: AFPluginData, + data: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .open_workspace(&workspace_id, AuthType::from(params.workspace_auth_type)) - .await?; + manager.open_workspace(¶ms.workspace_id).await?; Ok(()) } -#[tracing::instrument(level = "info", skip(data, manager), err)] -pub async fn get_user_workspace_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let params = data.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let uid = manager.user_id()?; - let user_workspace = manager.get_user_workspace_from_db(uid, &workspace_id)?; - data_result_ok(UserWorkspacePB::from(user_workspace)) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn update_network_state_handler( data: AFPluginData, @@ -476,12 +526,12 @@ pub async fn update_network_state_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let reachable = data.into_inner().ty.is_reachable(); - manager.cloud_service.set_network_reachable(reachable); + manager.cloud_services.set_network_reachable(reachable); manager .user_status_callback .read() .await - .on_network_status_changed(reachable); + .did_update_network(reachable); Ok(()) } @@ -546,6 +596,24 @@ pub async fn get_all_reminder_event_handler( data_result_ok(reminders.into()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn reset_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let reset_pb = data.into_inner(); + if reset_pb.workspace_id.is_empty() { + return Err(FlowyError::new( + ErrorCode::WorkspaceInitializeError, + "The workspace id is empty", + )); + } + let _session = manager.get_session()?; + manager.reset_workspace(reset_pb).await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn remove_reminder_event_handler( data: AFPluginData, @@ -577,9 +645,8 @@ pub async fn delete_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .remove_workspace_member(data.email, workspace_id) + .remove_workspace_member(data.email, data.workspace_id) .await?; Ok(()) } @@ -591,9 +658,8 @@ pub async fn get_workspace_members_handler( ) -> DataResult { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; let members = manager - .get_workspace_members(workspace_id) + .get_workspace_members(data.workspace_id) .await? .into_iter() .map(WorkspaceMemberPB::from) @@ -608,9 +674,8 @@ pub async fn update_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .update_workspace_member(data.email, workspace_id, data.role.into()) + .update_workspace_member(data.email, data.workspace_id, data.role.into()) .await?; Ok(()) } @@ -621,10 +686,9 @@ pub async fn create_workspace_handler( manager: AFPluginState>, ) -> DataResult { let data = data.try_into_inner()?; - let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; - let new_workspace = manager.create_workspace(&data.name, auth_type).await?; - data_result_ok(UserWorkspacePB::from(new_workspace)) + let new_workspace = manager.add_workspace(&data.name).await?; + data_result_ok(new_workspace.into()) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -634,7 +698,6 @@ pub async fn delete_workspace_handler( ) -> Result<(), FlowyError> { let workspace_id = delete_workspace_param.try_into_inner()?.workspace_id; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&workspace_id)?; manager.delete_workspace(&workspace_id).await?; Ok(()) } @@ -646,15 +709,9 @@ pub async fn rename_workspace_handler( ) -> Result<(), FlowyError> { let params = rename_workspace_param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let changeset = UserWorkspaceChangeset { - id: params.workspace_id, - name: Some(params.new_name), - icon: None, - role: None, - member_count: None, - }; - manager.patch_workspace(&workspace_id, changeset).await?; + manager + .patch_workspace(¶ms.workspace_id, Some(¶ms.new_name), None) + .await?; Ok(()) } @@ -665,15 +722,9 @@ pub async fn change_workspace_icon_handler( ) -> Result<(), FlowyError> { let params = change_workspace_icon_param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let changeset = UserWorkspaceChangeset { - id: workspace_id.to_string(), - name: None, - icon: Some(params.new_icon), - role: None, - member_count: None, - }; - manager.patch_workspace(&workspace_id, changeset).await?; + manager + .patch_workspace(¶ms.workspace_id, None, Some(¶ms.new_icon)) + .await?; Ok(()) } @@ -684,9 +735,8 @@ pub async fn invite_workspace_member_handler( ) -> Result<(), FlowyError> { let param = param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶m.workspace_id)?; manager - .invite_member_to_workspace(workspace_id, param.invitee_email, param.role.into()) + .invite_member_to_workspace(param.workspace_id, param.invitee_email, param.role.into()) .await?; Ok(()) } @@ -722,7 +772,6 @@ pub async fn leave_workspace_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let workspace_id = param.into_inner().workspace_id; - let workspace_id = Uuid::from_str(&workspace_id)?; let manager = upgrade_manager(manager)?; manager.leave_workspace(&workspace_id).await?; Ok(()) @@ -770,9 +819,9 @@ pub async fn get_workspace_usage_handler( param: AFPluginData, manager: AFPluginState>, ) -> DataResult { - let workspace_id = Uuid::from_str(¶m.into_inner().workspace_id)?; + let workspace_id = param.into_inner().workspace_id; let manager = upgrade_manager(manager)?; - let workspace_usage = manager.get_workspace_usage(&workspace_id).await?; + let workspace_usage = manager.get_workspace_usage(workspace_id).await?; data_result_ok(WorkspaceUsagePB::from(workspace_usage)) } @@ -790,12 +839,11 @@ pub async fn update_workspace_subscription_payment_period_handler( params: AFPluginData, manager: AFPluginState>, ) -> FlowyResult<()> { - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let params = params.try_into_inner()?; let manager = upgrade_manager(manager)?; manager .update_workspace_subscription_payment_period( - &workspace_id, + params.workspace_id, params.plan.into(), params.recurring_interval.into(), ) @@ -822,15 +870,12 @@ pub async fn get_workspace_member_info( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let workspace_id = manager.get_session()?.user_workspace.workspace_id()?; - let member = manager - .get_workspace_member_info(param.uid, &workspace_id) - .await?; + let member = manager.get_workspace_member_info(param.uid).await?; data_result_ok(member.into()) } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn update_workspace_setting_handler( +pub async fn update_workspace_setting( params: AFPluginData, manager: AFPluginState>, ) -> Result<(), FlowyError> { @@ -841,14 +886,13 @@ pub async fn update_workspace_setting_handler( } #[tracing::instrument(level = "info", skip_all, err)] -pub async fn get_workspace_setting_handler( +pub async fn get_workspace_setting( params: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let params = params.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; let manager = upgrade_manager(manager)?; - let pb = manager.get_workspace_settings(&workspace_id).await?; + let pb = manager.get_workspace_settings(¶ms.workspace_id).await?; data_result_ok(pb) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index ba242e6d46..0807b46170 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,13 +1,13 @@ use client_api::entity::billing_dto::SubscriptionPlan; +use std::sync::Weak; +use strum_macros::Display; + use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; use lib_infra::async_trait::async_trait; -use std::sync::Weak; -use strum_macros::Display; -use uuid::Uuid; use crate::event_handler::*; use crate::user_manager::UserManager; @@ -35,11 +35,12 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetUserSetting, get_user_setting) .event(UserEvent::SetCloudConfig, set_cloud_config_handler) .event(UserEvent::GetCloudConfig, get_cloud_config_handler) + .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler) + .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler) .event(UserEvent::OauthSignIn, oauth_sign_in_handler) .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) - .event(UserEvent::GetUserWorkspace, get_user_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::OpenAnonUser, open_anon_user_handler) .event(UserEvent::GetAnonUser, get_anon_user_handler) @@ -48,6 +49,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetAllReminders, get_all_reminder_event_handler) .event(UserEvent::RemoveReminder, remove_reminder_event_handler) .event(UserEvent::UpdateReminder, update_reminder_event_handler) + .event(UserEvent::ResetWorkspace, reset_workspace_handler) .event(UserEvent::SetDateTimeSettings, set_date_time_settings) .event(UserEvent::GetDateTimeSettings, get_date_time_settings) .event(UserEvent::SetNotificationSettings, set_notification_settings) @@ -76,21 +78,21 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::UpdateWorkspaceSubscriptionPaymentPeriod, update_workspace_subscription_payment_period_handler) .event(UserEvent::GetSubscriptionPlanDetails, get_subscription_plan_details_handler) // Workspace Setting - .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting_handler) - .event(UserEvent::GetWorkspaceSetting, get_workspace_setting_handler) + .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting) + .event(UserEvent::GetWorkspaceSetting, get_workspace_setting) .event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler) - .event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler) + } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Logging into an account using a register email and password - #[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")] + #[event(input = "SignInPayloadPB", output = "UserProfilePB")] SignInWithEmailPassword = 0, - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, @@ -128,7 +130,7 @@ pub enum UserEvent { OauthSignIn = 10, /// Get the OAuth callback url - /// Only use when the [AuthType] is AFCloud + /// Only use when the [Authenticator] is AFCloud #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")] GenerateSignInURL = 11, @@ -141,16 +143,19 @@ pub enum UserEvent { #[event(output = "CloudSettingPB")] GetCloudConfig = 14, + #[event(input = "UserSecretPB")] + SetEncryptionSecret = 15, + + #[event(output = "UserEncryptionConfigurationPB")] + CheckEncryptionSign = 16, + /// Return the all the workspaces of the user #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, - #[event(input = "OpenUserWorkspacePB")] + #[event(input = "UserWorkspaceIdPB")] OpenWorkspace = 21, - #[event(input = "UserWorkspaceIdPB", output = "UserWorkspacePB")] - GetUserWorkspace = 22, - #[event(input = "NetworkStatePB")] UpdateNetworkState = 24, @@ -161,7 +166,7 @@ pub enum UserEvent { OpenAnonUser = 26, /// Push a realtime event to the user. Currently, the realtime event - /// is only used when the auth type is: [AuthType::Supabase]. + /// is only used when the auth type is: [Authenticator::Supabase]. /// #[event(input = "RealtimePayloadPB")] PushRealtimeEvent = 27, @@ -178,6 +183,9 @@ pub enum UserEvent { #[event(input = "ReminderPB")] UpdateReminder = 31, + #[event(input = "ResetWorkspacePB")] + ResetWorkspace = 32, + /// Change the Date/Time formats globally #[event(input = "DateTimeSettingsPB")] SetDateTimeSettings = 33, @@ -253,7 +261,7 @@ pub enum UserEvent { #[event(input = "UpdateUserWorkspaceSettingPB")] UpdateWorkspaceSetting = 57, - #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSettingsPB")] + #[event(input = "UserWorkspaceIdPB", output = "UseAISettingPB")] GetWorkspaceSetting = 58, #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")] @@ -270,73 +278,62 @@ pub enum UserEvent { #[event()] DeleteAccount = 64, - - #[event(input = "PasscodeSignInPB", output = "GotrueTokenResponsePB")] - PasscodeSignIn = 65, } #[async_trait] pub trait UserStatusCallback: Send + Sync + 'static { - /// When the [AuthType] changed, this method will be called. Currently, the auth type + /// When the [Authenticator] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn on_auth_type_changed(&self, _authenticator: AuthType) {} - /// Fires on app launch, but only if the user is already signed in. - async fn on_launch_if_authenticated( + fn authenticator_did_changed(&self, _authenticator: Authenticator) {} + /// This will be called after the application launches if the user is already signed in. + /// If the user is not signed in, this method will not be called + async fn did_init( &self, _user_id: i64, + _user_authenticator: &Authenticator, _cloud_config: &Option, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, + _authenticator: &Authenticator, ) -> FlowyResult<()> { Ok(()) } - - async fn did_launch(&self) -> FlowyResult<()> { - Ok(()) - } - - /// Fires right after the user successfully signs in. - async fn on_sign_in( + /// Will be called after the user signed in. + async fn did_sign_in( &self, _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, + _authenticator: &Authenticator, ) -> FlowyResult<()> { Ok(()) } - - /// Fires right after the user successfully signs up. - async fn on_sign_up( + /// Will be called after the user signed up. + async fn did_sign_up( &self, _is_new_user: bool, _user_profile: &UserProfile, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, + _authenticator: &Authenticator, ) -> FlowyResult<()> { Ok(()) } - /// Fires when an authentication token has expired. - async fn on_token_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { + async fn did_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { Ok(()) } - - /// Fires when a workspace is opened by the user. - async fn on_workspace_opened( + async fn open_workspace( &self, _user_id: i64, - _workspace_id: &Uuid, _user_workspace: &UserWorkspace, - _auth_type: &AuthType, + _authenticator: &Authenticator, ) -> FlowyResult<()> { Ok(()) } - fn on_network_status_changed(&self, _reachable: bool) {} - fn on_subscription_plans_updated(&self, _plans: Vec) {} - fn on_storage_permission_updated(&self, _can_write: bool) {} + fn did_update_network(&self, _reachable: bool) {} + fn did_update_plans(&self, _plans: Vec) {} + fn did_update_storage_limitation(&self, _can_write: bool) {} } /// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function diff --git a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs deleted file mode 100644 index 1b1c3f890f..0000000000 --- a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs +++ /dev/null @@ -1,50 +0,0 @@ -use diesel::SqliteConnection; -use semver::Version; -use std::sync::Arc; -use tracing::instrument; - -use collab_integrate::CollabKVDB; -use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; - -use crate::migrations::migration::UserDataMigration; -use flowy_user_pub::session::Session; -use flowy_user_pub::sql::upsert_user_workspace; - -pub struct AnonUserWorkspaceTableMigration; - -impl UserDataMigration for AnonUserWorkspaceTableMigration { - fn name(&self) -> &str { - "anon_user_workspace_table_migration" - } - - fn run_when( - &self, - first_installed_version: &Option, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => true, - Some(version) => version <= &Version::new(0, 8, 10), - } - } - - #[instrument(name = "AnonUserWorkspaceTableMigration", skip_all, err)] - fn run( - &self, - session: &Session, - _collab_db: &Arc, - user_auth_type: &AuthType, - db: &mut SqliteConnection, - ) -> FlowyResult<()> { - // For historical reason, anon user doesn't have a workspace in user_workspace_table. - // So we need to create a new entry for the anon user in the user_workspace_table. - if matches!(user_auth_type, AuthType::Local) { - let mut user_workspace = session.user_workspace.clone(); - user_workspace.workspace_type = AuthType::Local; - upsert_user_workspace(session.user_id, *user_auth_type, user_workspace, db)?; - } - - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs index 735d8f1f49..3056f4d945 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use collab_plugins::local_storage::kv::doc::migrate_old_keys; use collab_plugins::local_storage::kv::KVTransactionDB; -use diesel::SqliteConnection; use semver::Version; use tracing::{instrument, trace}; use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use flowy_user_pub::session::Session; @@ -40,8 +39,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { &self, session: &Session, collab_db: &Arc, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, + _authenticator: &Authenticator, ) -> FlowyResult<()> { trace!( "migrate key with workspace id:{}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index 996386cb5e..e557c22450 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -6,13 +6,12 @@ use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::{Folder, View}; use collab_plugins::local_storage::kv::KVTransactionDB; -use diesel::SqliteConnection; use semver::Version; use tracing::{event, instrument}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -42,13 +41,12 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { &self, session: &Session, collab_db: &Arc, - user_auth_type: &AuthType, - _db: &mut SqliteConnection, + authenticator: &Authenticator, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. // - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version. - if !matches!(user_auth_type, AuthType::Local) { + if !matches!(authenticator, Authenticator::Local) { return Ok(()); } collab_db.with_write_txn(|write_txn| { diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 1cf8d6a943..c604e47e8d 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -7,7 +7,7 @@ use flowy_error::FlowyResult; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_data_migration_records; use flowy_sqlite::ConnectionPool; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use flowy_user_pub::session::Session; use semver::Version; use tracing::info; @@ -54,7 +54,7 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec>, - user_auth_type: &AuthType, + authenticator: &Authenticator, app_version: &Version, ) -> FlowyResult> { let mut applied_migrations = vec![]; @@ -75,7 +75,7 @@ impl UserLocalDataMigration { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - migration.run(&self.session, &self.collab_db, user_auth_type, &mut conn)?; + migration.run(&self.session, &self.collab_db, authenticator)?; applied_migrations.push(migration.name().to_string()); save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); @@ -98,8 +98,7 @@ pub trait UserDataMigration { &self, user: &Session, collab_db: &Arc, - user_auth_type: &AuthType, - db: &mut SqliteConnection, + authenticator: &Authenticator, ) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index 3d87dc595f..c8d04edf66 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,7 +1,6 @@ use flowy_user_pub::session::Session; use std::sync::Arc; -pub mod anon_user_workspace; pub mod doc_key_with_workspace; pub mod document_empty_content; pub mod migration; diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index d3cea0e976..ec55b5fe29 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; -use diesel::SqliteConnection; use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -40,8 +39,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { &self, session: &Session, collab_db: &Arc, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, + _authenticator: &Authenticator, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index ee9156199e..d631e32e78 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; -use diesel::SqliteConnection; use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -38,8 +37,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { &self, session: &Session, collab_db: &Arc, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, + _authenticator: &Authenticator, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { if let Ok(collab) = load_collab( diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index dd93593468..a8bd91b55b 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -14,7 +14,7 @@ pub(crate) enum UserNotification { DidUpdateUserWorkspaces = 3, DidUpdateCloudConfig = 4, DidUpdateUserWorkspace = 5, - DidUpdateWorkspaceSetting = 6, + DidUpdateAISetting = 6, } impl std::convert::From for i32 { diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 418f0638d3..7d770e8123 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -1,22 +1,22 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; +use crate::services::sqlite_sql::user_sql::vacuum_database; use collab_integrate::CollabKVDB; -use crate::user_manager::manager_history_user::ANON_USER; use arc_swap::ArcSwapOption; use collab_plugins::local_storage::kv::doc::CollabKVAction; use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::DBConnection; -use flowy_user_pub::entities::{AuthType, UserWorkspace}; +use flowy_user_pub::entities::UserWorkspace; use flowy_user_pub::session::Session; use std::path::PathBuf; -use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::info; -use uuid::Uuid; +use tracing::{error, info}; + +const SQLITE_VACUUM_042: &str = "sqlite_vacuum_042_version"; pub struct AuthenticateUser { pub user_config: UserConfig, @@ -42,33 +42,40 @@ impl AuthenticateUser { } } + pub fn vacuum_database_if_need(&self) { + if !self + .store_preferences + .get_bool_or_default(SQLITE_VACUUM_042) + { + if let Ok(session) = self.get_session() { + let _ = self.store_preferences.set_bool(SQLITE_VACUUM_042, true); + if let Ok(conn) = self.database.get_connection(session.user_id) { + info!("vacuum database 042"); + if let Err(err) = vacuum_database(conn) { + error!("vacuum database error: {:?}", err); + } + } + } + } + } + pub fn user_id(&self) -> FlowyResult { let session = self.get_session()?; Ok(session.user_id) } - pub async fn is_local_mode(&self) -> FlowyResult { - let session = self.get_session()?; - Ok(matches!( - session.user_workspace.workspace_type, - AuthType::Local - )) - } - pub fn device_id(&self) -> FlowyResult { Ok(self.user_config.device_id.to_string()) } - pub fn workspace_id(&self) -> FlowyResult { + pub fn workspace_id(&self) -> FlowyResult { let session = self.get_session()?; - let workspace_uuid = Uuid::from_str(&session.user_workspace.id)?; - Ok(workspace_uuid) + Ok(session.user_workspace.id.clone()) } - pub fn workspace_database_object_id(&self) -> FlowyResult { + pub fn workspace_database_object_id(&self) -> FlowyResult { let session = self.get_session()?; - let id = Uuid::from_str(&session.user_workspace.workspace_database_id)?; - Ok(id) + Ok(session.user_workspace.workspace_database_id.clone()) } pub fn get_collab_db(&self, uid: i64) -> FlowyResult> { @@ -82,9 +89,9 @@ impl AuthenticateUser { self.database.get_connection(uid) } - pub fn get_index_path(&self) -> FlowyResult { - let uid = self.user_id()?; - Ok(PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes")) + pub fn get_index_path(&self) -> PathBuf { + let uid = self.user_id().unwrap_or(0); + PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes") } pub fn get_user_data_dir(&self) -> FlowyResult { @@ -147,21 +154,13 @@ impl AuthenticateUser { match self .store_preferences - .get_object::(&self.user_config.session_cache_key) + .get_object::>(&self.user_config.session_cache_key) { None => Err(FlowyError::new( ErrorCode::RecordNotFound, - "Can't find user session. Please login again", + "User is not logged in", )), - Some(mut session) => { - // Set the workspace type to local if the user is anon. - if let Some(anon_session) = self.store_preferences.get_object::(ANON_USER) { - if session.user_id == anon_session.user_id { - session.user_workspace.workspace_type = AuthType::Local; - } - } - - let session = Arc::new(session); + Some(session) => { self.session.store(Some(session.clone())); Ok(session) }, diff --git a/frontend/rust-lib/flowy-user/src/services/billing_check.rs b/frontend/rust-lib/flowy-user/src/services/billing_check.rs index ea5bc65b75..b0b123fc41 100644 --- a/frontend/rust-lib/flowy-user/src/services/billing_check.rs +++ b/frontend/rust-lib/flowy-user/src/services/billing_check.rs @@ -4,7 +4,6 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_user_pub::cloud::UserCloudServiceProvider; use std::sync::Weak; use std::time::Duration; -use uuid::Uuid; /// `PeriodicallyCheckBillingState` is designed to periodically verify the subscription /// plan of a given workspace. It utilizes a cloud service provider to fetch the current @@ -14,7 +13,7 @@ use uuid::Uuid; /// at specified intervals until the expected plan is found or the maximum number of /// attempts is reached. pub struct PeriodicallyCheckBillingState { - workspace_id: Uuid, + workspace_id: String, cloud_service: Weak, expected_plan: Option, user: Weak, @@ -22,7 +21,7 @@ pub struct PeriodicallyCheckBillingState { impl PeriodicallyCheckBillingState { pub fn new( - workspace_id: Uuid, + workspace_id: String, expected_plan: Option, cloud_service: Weak, user: Weak, @@ -47,7 +46,7 @@ impl PeriodicallyCheckBillingState { while attempts < max_attempts { let plans = cloud_service .get_user_service()? - .get_workspace_plan(self.workspace_id) + .get_workspace_plan(self.workspace_id.clone()) .await?; // If the expected plan is not set, return the plans immediately. Otherwise, diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 90113b8062..129b605281 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -3,7 +3,8 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::data_import::importer::load_collab_by_object_ids; use crate::services::db::UserDBPath; use crate::services::entities::UserPaths; -use crate::user_manager::run_data_migration; +use crate::services::sqlite_sql::user_sql::select_user_profile; +use crate::user_manager::run_collab_data_migration; use anyhow::anyhow; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -29,22 +30,19 @@ use flowy_folder_pub::entities::{ }; use flowy_sqlite::kv::KVStorePreferences; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; -use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; +use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; use flowy_user_pub::session::Session; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use collab_document::blocks::TextDelta; use collab_document::document::Document; -use flowy_user_pub::sql::{select_user_auth_type, select_user_profile}; use semver::Version; use serde_json::json; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::sync::{Arc, Weak}; use tracing::{error, event, info, instrument, warn}; -use uuid::Uuid; - pub(crate) struct ImportedFolder { pub imported_session: Session, pub imported_collab_db: Arc, @@ -101,19 +99,14 @@ pub(crate) fn prepare_import( CollabKVDB::open(collab_db_path) .map_err(|err| anyhow!("[AppflowyData]: open import collab db failed: {:?}", err))?, ); - - let mut conn = imported_sqlite_db.get_connection()?; - let imported_user_auth_type = select_user_profile( + let imported_user = select_user_profile( imported_session.user_id, - &imported_session.user_workspace.id, - &mut conn, - ) - .map(|v| v.auth_type) - .or_else(|_| select_user_auth_type(imported_session.user_id, &mut conn))?; + imported_sqlite_db.get_connection()?, + )?; - run_data_migration( + run_collab_data_migration( &imported_session, - &imported_user_auth_type, + &imported_user, imported_collab_db.clone(), imported_sqlite_db.get_pool(), other_store_preferences.clone(), @@ -1179,8 +1172,8 @@ impl DerefMut for OldToNewIdMap { pub async fn upload_collab_objects_data( uid: i64, user_collab_db: Weak, - workspace_id: &Uuid, - user_authenticator: &AuthType, + workspace_id: &str, + user_authenticator: &Authenticator, collab_data: ImportedCollabData, user_cloud_service: Arc, ) -> Result<(), FlowyError> { @@ -1255,7 +1248,7 @@ pub async fn upload_collab_objects_data( objects.push(UserCollabParams { object_id: oid, encoded_collab, - collab_type, + collab_type: collab_type.clone(), }); size_counter += obj_size; } @@ -1282,7 +1275,7 @@ pub async fn upload_collab_objects_data( async fn batch_create( uid: i64, - workspace_id: &Uuid, + workspace_id: &str, user_cloud_service: &Arc, size_counter: &usize, objects: Vec, diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 15126558d7..e324c2820f 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -7,13 +7,21 @@ use collab_plugins::local_storage::kv::KVTransactionDB; use dashmap::mapref::entry::Entry; use dashmap::DashMap; use flowy_error::FlowyError; +use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::ConnectionPool; -use flowy_sqlite::{DBConnection, Database}; -use flowy_user_pub::entities::UserProfile; -use flowy_user_pub::sql::select_user_profile; +use flowy_sqlite::{ + query_dsl::*, + schema::{user_table, user_table::dsl}, + DBConnection, Database, ExpressionMethods, +}; +use flowy_user_pub::entities::{UserProfile, UserWorkspace}; + use lib_infra::file_util::{unzip_and_replace, zip_folder}; use tracing::{error, event, info, instrument}; +use crate::services::sqlite_sql::user_sql::UserTable; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; + pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; fn collab_db_path(&self, uid: i64) -> PathBuf; @@ -125,11 +133,26 @@ impl UserDB { &self, pool: &Arc, uid: i64, - workspace_id: &str, ) -> Result { + let uid = uid.to_string(); let mut conn = pool.get()?; - let profile = select_user_profile(uid, workspace_id, &mut conn)?; - Ok(profile) + let user = dsl::user_table + .filter(user_table::id.eq(&uid)) + .first::(&mut *conn)?; + + Ok(user.into()) + } + + pub fn get_user_workspace( + &self, + pool: &Arc, + uid: i64, + ) -> Result, FlowyError> { + let mut conn = pool.get()?; + let row = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .first::(&mut *conn)?; + Ok(Some(UserWorkspace::from(row))) } /// Open a collab db for the user. If the db is already opened, return the opened db. diff --git a/frontend/rust-lib/flowy-user/src/services/mod.rs b/frontend/rust-lib/flowy-user/src/services/mod.rs index ab4b3bea37..66316fa01a 100644 --- a/frontend/rust-lib/flowy-user/src/services/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/mod.rs @@ -5,3 +5,4 @@ pub mod collab_interact; pub mod data_import; pub mod db; pub mod entities; +pub mod sqlite_sql; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs similarity index 71% rename from frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs rename to frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs index 58ca65e732..70351ab105 100644 --- a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs @@ -1,11 +1,12 @@ -use crate::entities::{Role, WorkspaceMember}; use diesel::{insert_into, RunQueryDsl}; use flowy_error::FlowyResult; -use flowy_sqlite::schema::workspace_members_table; -use flowy_sqlite::schema::workspace_members_table::dsl; -use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; -#[derive(Queryable, Insertable, AsChangeset, Debug, Clone)] +use flowy_sqlite::schema::workspace_members_table; + +use flowy_sqlite::schema::workspace_members_table::dsl; +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; + +#[derive(Queryable, Insertable, AsChangeset, Debug)] #[diesel(table_name = workspace_members_table)] #[diesel(primary_key(email, workspace_id))] pub struct WorkspaceMemberTable { @@ -18,19 +19,8 @@ pub struct WorkspaceMemberTable { pub updated_at: chrono::NaiveDateTime, } -impl From for WorkspaceMember { - fn from(value: WorkspaceMemberTable) -> Self { - Self { - email: value.email, - role: Role::from(value.role), - name: value.name, - avatar_url: value.avatar_url, - } - } -} - pub fn upsert_workspace_member>( - conn: &mut SqliteConnection, + mut conn: DBConnection, member: T, ) -> FlowyResult<()> { let member = member.into(); @@ -43,7 +33,7 @@ pub fn upsert_workspace_member>( )) .do_update() .set(&member) - .execute(conn)?; + .execute(&mut conn)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs new file mode 100644 index 0000000000..93e642f72e --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod member_sql; +pub(crate) mod user_sql; +pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs new file mode 100644 index 0000000000..6da6f183cb --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -0,0 +1,157 @@ +use diesel::{sql_query, RunQueryDsl}; +use flowy_error::{internal_error, FlowyError}; +use std::str::FromStr; + +use flowy_user_pub::cloud::UserUpdate; +use flowy_user_pub::entities::*; + +use flowy_sqlite::schema::user_table; + +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +/// The order of the fields in the struct must be the same as the order of the fields in the table. +/// Check out the [schema.rs] for table schema. +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_table)] +pub struct UserTable { + pub(crate) id: String, + pub(crate) name: String, + #[deprecated( + note = "The workspace_id is deprecated, please use the [Session::UserWorkspace] instead" + )] + pub(crate) workspace: String, + pub(crate) icon_url: String, + pub(crate) openai_key: String, + pub(crate) token: String, + pub(crate) email: String, + pub(crate) auth_type: i32, + pub(crate) encryption_type: String, + pub(crate) stability_ai_key: String, + pub(crate) updated_at: i64, + pub(crate) ai_model: String, +} + +#[allow(deprecated)] +impl From<(UserProfile, Authenticator)> for UserTable { + fn from(value: (UserProfile, Authenticator)) -> Self { + let (user_profile, auth_type) = value; + let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); + UserTable { + id: user_profile.uid.to_string(), + name: user_profile.name, + #[allow(deprecated)] + workspace: "".to_string(), + icon_url: user_profile.icon_url, + openai_key: user_profile.openai_key, + token: user_profile.token, + email: user_profile.email, + auth_type: auth_type as i32, + encryption_type, + stability_ai_key: user_profile.stability_ai_key, + updated_at: user_profile.updated_at, + ai_model: user_profile.ai_model, + } + } +} + +impl From for UserProfile { + fn from(table: UserTable) -> Self { + UserProfile { + uid: table.id.parse::().unwrap_or(0), + email: table.email, + name: table.name, + token: table.token, + icon_url: table.icon_url, + openai_key: table.openai_key, + authenticator: Authenticator::from(table.auth_type), + encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), + stability_ai_key: table.stability_ai_key, + updated_at: table.updated_at, + ai_model: table.ai_model, + } + } +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_table)] +pub struct UserTableChangeset { + pub id: String, + pub workspace: Option, // deprecated + pub name: Option, + pub email: Option, + pub icon_url: Option, + pub openai_key: Option, + pub encryption_type: Option, + pub token: Option, + pub stability_ai_key: Option, + pub ai_model: Option, +} + +impl UserTableChangeset { + pub fn new(params: UpdateUserProfileParams) -> Self { + let encryption_type = params.encryption_sign.map(|sign| { + let ty = EncryptionType::from_sign(&sign); + serde_json::to_string(&ty).unwrap_or_default() + }); + UserTableChangeset { + id: params.uid.to_string(), + workspace: None, + name: params.name, + email: params.email, + icon_url: params.icon_url, + openai_key: params.openai_key, + encryption_type, + token: params.token, + stability_ai_key: params.stability_ai_key, + ai_model: params.ai_model, + } + } + + pub fn from_user_profile(user_profile: UserProfile) -> Self { + let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); + UserTableChangeset { + id: user_profile.uid.to_string(), + workspace: None, + name: Some(user_profile.name), + email: Some(user_profile.email), + icon_url: Some(user_profile.icon_url), + openai_key: Some(user_profile.openai_key), + encryption_type: Some(encryption_type), + token: Some(user_profile.token), + stability_ai_key: Some(user_profile.stability_ai_key), + ai_model: Some(user_profile.ai_model), + } + } +} + +impl From for UserTableChangeset { + fn from(value: UserUpdate) -> Self { + UserTableChangeset { + id: value.uid.to_string(), + name: value.name, + email: value.email, + ..Default::default() + } + } +} + +pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result { + let user: UserProfile = user_table::dsl::user_table + .filter(user_table::id.eq(&uid.to_string())) + .first::(&mut *conn) + .map_err(|err| { + FlowyError::record_not_found().with_context(format!( + "Can't find the user profile for user id: {}, error: {:?}", + uid, err + )) + })? + .into(); + + Ok(user) +} + +pub(crate) fn vacuum_database(mut conn: DBConnection) -> Result<(), FlowyError> { + sql_query("VACUUM") + .execute(&mut *conn) + .map_err(internal_error)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs new file mode 100644 index 0000000000..8d5c1e8dc7 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -0,0 +1,131 @@ +use chrono::{TimeZone, Utc}; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::FlowyError; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use flowy_user_pub::entities::UserWorkspace; +use std::convert::TryFrom; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceTable { + pub id: String, + pub name: String, + pub uid: i64, + pub created_at: i64, + pub database_storage_id: String, + pub icon: String, + pub member_count: i64, + pub role: Option, +} + +pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option { + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(workspace_id)) + .first::(&mut *conn) + .ok() + .map(UserWorkspace::from) +} + +pub fn get_all_user_workspace_op( + user_id: i64, + mut conn: DBConnection, +) -> Result, FlowyError> { + let rows = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(user_id)) + .load::(&mut *conn)?; + Ok(rows.into_iter().map(UserWorkspace::from).collect()) +} + +/// Remove all existing workspaces for given user and insert the new ones. +/// +#[allow(dead_code)] +pub fn save_user_workspaces_op( + uid: i64, + mut conn: DBConnection, + user_workspaces: &[UserWorkspace], +) -> Result<(), FlowyError> { + conn.immediate_transaction(|conn| { + delete_existing_workspaces(uid, conn)?; + insert_or_update_workspaces_op(uid, user_workspaces, conn)?; + Ok(()) + }) +} + +#[allow(dead_code)] +fn delete_existing_workspaces(uid: i64, conn: &mut SqliteConnection) -> Result<(), FlowyError> { + diesel::delete( + user_workspace_table::dsl::user_workspace_table.filter(user_workspace_table::uid.eq(uid)), + ) + .execute(conn)?; + Ok(()) +} + +pub fn insert_or_update_workspaces_op( + uid: i64, + user_workspaces: &[UserWorkspace], + conn: &mut SqliteConnection, +) -> Result<(), FlowyError> { + for user_workspace in user_workspaces { + let new_record = UserWorkspaceTable::try_from((uid, user_workspace))?; + + diesel::insert_into(user_workspace_table::table) + .values(new_record.clone()) + .on_conflict(user_workspace_table::id) + .do_update() + .set(( + user_workspace_table::name.eq(new_record.name), + user_workspace_table::uid.eq(new_record.uid), + user_workspace_table::created_at.eq(new_record.created_at), + user_workspace_table::database_storage_id.eq(new_record.database_storage_id), + user_workspace_table::icon.eq(new_record.icon), + user_workspace_table::member_count.eq(new_record.member_count), + user_workspace_table::role.eq(new_record.role), + )) + .execute(conn)?; + } + + Ok(()) +} + +impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { + type Error = FlowyError; + + fn try_from(value: (i64, &UserWorkspace)) -> Result { + if value.1.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if value.1.workspace_database_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: value.1.id.clone(), + name: value.1.name.clone(), + uid: value.0, + created_at: value.1.created_at.timestamp(), + database_storage_id: value.1.workspace_database_id.clone(), + icon: value.1.icon.clone(), + member_count: value.1.member_count, + role: value.1.role.clone().map(|v| v as i32), + }) + } +} + +impl From for UserWorkspace { + fn from(value: UserWorkspaceTable) -> Self { + Self { + id: value.id, + name: value.name, + created_at: Utc + .timestamp_opt(value.created_at, 0) + .single() + .unwrap_or_default(), + workspace_database_id: value.database_storage_id, + icon: value.icon, + member_count: value.member_count, + role: value.role.map(|v| v.into()), + } + } +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index b95ac3baaf..f04c988f5c 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,12 +1,12 @@ -use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::FlowyResult; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; use arc_swap::ArcSwapOption; use collab::lock::RwLock; use collab_user::core::UserAwareness; use dashmap::DashMap; +use flowy_server_pub::AuthenticatorType; use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_table; use flowy_sqlite::ConnectionPool; @@ -14,15 +14,16 @@ use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudServiceProvider, UserUpdate}; use flowy_user_pub::entities::*; use flowy_user_pub::workspace_service::UserWorkspaceService; -use lib_infra::box_any::BoxAny; use semver::Version; use serde_json::Value; use std::string::ToString; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; +use tokio::sync::Mutex; use tokio_stream::StreamExt; use tracing::{debug, error, event, info, instrument, warn}; -use uuid::Uuid; + +use lib_infra::box_any::BoxAny; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserStatusCallback}; @@ -37,23 +38,27 @@ use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; -use crate::migrations::anon_user_workspace::AnonUserWorkspaceTableMigration; +use super::manager_user_workspace::save_user_workspace; use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; +use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; +use crate::user_manager::manager_user_encryption::validate_encryption_sign; +use crate::user_manager::manager_user_workspace::save_all_user_workspaces; +use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; -use flowy_user_pub::sql::*; pub struct UserManager { - pub(crate) cloud_service: Arc, + pub(crate) cloud_services: Arc, pub(crate) store_preferences: Arc, pub(crate) user_awareness: Arc>>, pub(crate) user_status_callback: RwLock>, pub(crate) collab_builder: Weak, pub(crate) collab_interact: RwLock>, pub(crate) user_workspace_service: Arc, + auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, - pub(crate) is_loading_awareness: Arc>, + pub(crate) is_loading_awareness: Arc>, } impl UserManager { @@ -69,12 +74,13 @@ impl UserManager { let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { - cloud_service: cloud_services, + cloud_services, store_preferences, user_awareness: Default::default(), user_status_callback, collab_builder, collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), + auth_process: Default::default(), authenticate_user, refresh_user_profile_since, user_workspace_service, @@ -82,7 +88,7 @@ impl UserManager { }); let weak_user_manager = Arc::downgrade(&user_manager); - if let Ok(user_service) = user_manager.cloud_service.get_user_service() { + if let Ok(user_service) = user_manager.cloud_services.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { tokio::spawn(async move { while let Some(update) = rx.recv().await { @@ -127,19 +133,30 @@ impl UserManager { *self.collab_interact.write().await = Arc::new(collab_interact); if let Ok(session) = self.get_session() { - let user = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; - let auth_type = user.workspace_auth_type; - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; + let user = self.get_user_profile_from_disk(session.user_id).await?; + + // Get the current authenticator from the environment variable + let current_authenticator = current_authenticator(); + + // If the current authenticator is different from the authenticator in the session and it's + // not a local authenticator, we need to sign out the user. + if user.authenticator != Authenticator::Local && user.authenticator != current_authenticator { + event!( + tracing::Level::INFO, + "Authenticator changed from {:?} to {:?}", + user.authenticator, + current_authenticator + ); + self.sign_out().await?; + return Ok(()); + } event!( tracing::Level::INFO, - "init user session: {}:{}, auth type: {:?}", + "init user session: {}:{}, authenticator: {:?}", user.uid, user.email, - auth_type, + user.authenticator, ); self.prepare_user(&session).await; @@ -148,21 +165,24 @@ impl UserManager { // Set the token if the current cloud service using token to authenticate // Currently, only the AppFlowy cloud using token to init the client api. // TODO(nathan): using trait to separate the init process for different cloud service - if user.auth_type.is_appflowy_cloud() { - if let Err(err) = self.cloud_service.set_token(&user.token) { + if user.authenticator.is_appflowy_cloud() { + if let Err(err) = self.cloud_services.set_token(&user.token) { error!("Set token failed: {}", err); } + if let Err(err) = self.cloud_services.set_ai_model(&user.ai_model) { + error!("Set ai model failed: {}", err); + } + // Subscribe the token state - let weak_cloud_services = Arc::downgrade(&self.cloud_service); + let weak_cloud_services = Arc::downgrade(&self.cloud_services); let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); let cloned_session = session.clone(); - if let Some(mut token_state_rx) = self.cloud_service.subscribe_token_state() { + if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; let local_token = user.token.clone(); - let workspace_id = session.user_workspace.id.clone(); tokio::spawn(async move { while let Some(token_state) = token_state_rx.next().await { debug!("Token state changed: {:?}", token_state); @@ -172,7 +192,7 @@ impl UserManager { if new_token != local_token { if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { // Save the new token - if let Err(err) = save_user_token(user_uid, &workspace_id, conn, new_token) { + if let Err(err) = save_user_token(user_uid, conn, new_token) { error!("Save user token failed: {}", err); } } @@ -236,9 +256,9 @@ impl UserManager { self.authenticate_user.database.get_pool(session.user_id), ) { (Ok(collab_db), Ok(sqlite_pool)) => { - run_data_migration( + run_collab_data_migration( &session, - &user.auth_type, + &user, collab_db, sqlite_pool, self.store_preferences.clone(), @@ -247,20 +267,24 @@ impl UserManager { }, _ => error!("Failed to get collab db or sqlite pool"), } + self.authenticate_user.vacuum_database_if_need(); // migrations should run before set the first time installed version self.set_first_time_installed_version(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); // Init the user awareness. here we ignore the error - let _ = self.initial_user_awareness(&session, &auth_type).await; + let _ = self + .initial_user_awareness(&session, &user.authenticator) + .await; user_status_callback - .on_launch_if_authenticated( + .did_init( user.uid, + &user.authenticator, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, - &auth_type, + &user.authenticator, ) .await?; } else { @@ -325,12 +349,12 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - auth_type: AuthType, + authenticator: Authenticator, ) -> Result { - self.cloud_service.set_server_auth_type(&auth_type, None)?; + self.cloud_services.set_user_authenticator(&authenticator); let response: AuthResponse = self - .cloud_service + .cloud_services .get_user_service()? .sign_in(BoxAny::new(params)) .await?; @@ -338,21 +362,23 @@ impl UserManager { self.prepare_user(&session).await; let latest_workspace = response.latest_workspace.clone(); - let user_profile = UserProfile::from((&response, &auth_type)); - self.save_auth_data(&response, auth_type, &session).await?; + let user_profile = UserProfile::from((&response, &authenticator)); + self + .save_auth_data(&response, &authenticator, &session) + .await?; let _ = self - .initial_user_awareness(&session, &user_profile.workspace_auth_type) + .initial_user_awareness(&session, &user_profile.authenticator) .await; self .user_status_callback .read() .await - .on_sign_in( + .did_sign_in( user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, - &auth_type, + &authenticator, ) .await?; send_auth_state_notification(AuthStateChangedPB { @@ -372,46 +398,79 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_up( &self, - auth_type: AuthType, + authenticator: Authenticator, params: BoxAny, ) -> Result { - self.cloud_service.set_server_auth_type(&auth_type, None)?; - // sign out the current user if there is one - let migration_user = self.get_migration_user(&auth_type).await; - let auth_service = self.cloud_service.get_user_service()?; + let migration_user = self.get_migration_user(&authenticator).await; + + self.cloud_services.set_user_authenticator(&authenticator); + let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; - let new_user_profile = UserProfile::from((&response, &auth_type)); - self - .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) - .await?; + let new_user_profile = UserProfile::from((&response, &authenticator)); + if new_user_profile.encryption_type.require_encrypt_secret() { + self.auth_process.lock().await.replace(UserAuthProcess { + user_profile: new_user_profile.clone(), + migration_user, + response, + authenticator, + }); + } else { + self + .continue_sign_up(&new_user_profile, migration_user, response, &authenticator) + .await?; + } Ok(new_user_profile) } + #[tracing::instrument(level = "info", skip(self))] + pub async fn resume_sign_up(&self) -> Result<(), FlowyError> { + let UserAuthProcess { + user_profile, + migration_user, + response, + authenticator, + } = self + .auth_process + .lock() + .await + .clone() + .ok_or(FlowyError::new( + ErrorCode::Internal, + "No resumable sign up data", + ))?; + self + .continue_sign_up(&user_profile, migration_user, response, &authenticator) + .await?; + Ok(()) + } + #[tracing::instrument(level = "info", skip_all, err)] async fn continue_sign_up( &self, new_user_profile: &UserProfile, migration_user: Option, response: AuthResponse, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, *auth_type, &new_session) + .save_auth_data(&response, authenticator, &new_session) .await?; - let _ = self.initial_user_awareness(&new_session, auth_type).await; + let _ = self + .initial_user_awareness(&new_session, &new_user_profile.authenticator) + .await; self .user_status_callback .read() .await - .on_sign_up( + .did_sign_up( response.is_new_user, new_user_profile, &new_session.user_workspace, &self.authenticate_user.user_config.device_id, - auth_type, + authenticator, ) .await?; @@ -435,7 +494,7 @@ impl UserManager { new_user_profile.uid ); self - .migrate_anon_user_data_to_cloud(&old_user, &new_session, auth_type) + .migrate_anon_user_data_to_cloud(&old_user, &new_session, authenticator) .await?; self.remove_anon_user(); let _ = self @@ -456,7 +515,7 @@ impl UserManager { pub async fn sign_out(&self) -> Result<(), FlowyError> { if let Ok(session) = self.get_session() { sign_out( - &self.cloud_service, + &self.cloud_services, &session, &self.authenticate_user, self.db_connection(session.user_id)?, @@ -469,7 +528,7 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self))] pub async fn delete_account(&self) -> Result<(), FlowyError> { self - .cloud_service + .cloud_services .get_user_service()? .delete_account() .await?; @@ -491,16 +550,14 @@ impl UserManager { let session = self.get_session()?; upsert_user_profile_change( session.user_id, - &session.user_workspace.id, self.db_connection(session.user_id)?, changeset, )?; - self - .cloud_service - .get_user_service()? - .update_user(params) - .await?; + let profile = self.get_user_profile_from_disk(session.user_id).await?; + self + .update_user(session.user_id, profile.token, params) + .await?; Ok(()) } @@ -525,23 +582,14 @@ impl UserManager { } /// Fetches the user profile for the given user ID. - pub async fn get_user_profile_from_disk( - &self, - uid: i64, - workspace_id: &str, - ) -> Result { - let mut conn = self.db_connection(uid)?; - select_user_profile(uid, workspace_id, &mut conn) + pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result { + select_user_profile(uid, self.db_connection(uid)?) } #[tracing::instrument(level = "info", skip_all, err)] - pub async fn refresh_user_profile( - &self, - old_user_profile: &UserProfile, - workspace_id: &str, - ) -> FlowyResult<()> { + pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile - if old_user_profile.workspace_auth_type.is_local() { + if old_user_profile.authenticator.is_local() { return Ok(()); } @@ -554,20 +602,20 @@ impl UserManager { let uid = old_user_profile.uid; let result: Result = self - .cloud_service + .cloud_services .get_user_service()? - .get_user_profile(uid, workspace_id) + .get_user_profile(UserCredentials::from_uid(uid)) .await; match result { Ok(new_user_profile) => { // If the user profile is updated, save the new user profile if new_user_profile.updated_at > old_user_profile.updated_at { + validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); // Save the new user profile let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change( uid, - workspace_id, self.authenticate_user.database.get_connection(uid)?, changeset, ); @@ -601,16 +649,6 @@ impl UserManager { self.authenticate_user.user_paths.user_data_dir(uid) } - pub fn token_from_auth_type(&self, auth_type: &AuthType) -> FlowyResult> { - match auth_type { - AuthType::Local => Ok(None), - AuthType::AppFlowyCloud => { - let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - Ok(select_user_token(uid, &mut conn).ok()) - }, - } - } pub fn user_setting(&self) -> Result { let session = self.get_session()?; let user_setting = UserSettingPB { @@ -632,86 +670,79 @@ impl UserManager { Ok(None) } + async fn update_user( + &self, + uid: i64, + token: String, + params: UpdateUserProfileParams, + ) -> Result<(), FlowyError> { + let server = self.cloud_services.get_user_service()?; + tokio::spawn(async move { + let credentials = UserCredentials::new(Some(token), Some(uid), None); + server.update_user(credentials, params).await + }) + .await + .map_err(internal_error)??; + Ok(()) + } + async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { - let conn = self.db_connection(uid)?; - upsert_user(user, conn)?; + let mut conn = self.db_connection(uid)?; + conn.immediate_transaction(|conn| { + // delete old user if exists + diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) + .execute(conn)?; + + let _ = diesel::insert_into(user_table::table) + .values(user) + .execute(conn)?; + Ok::<(), FlowyError>(()) + })?; + Ok(()) } pub async fn receive_realtime_event(&self, json: Value) { - if let Ok(user_service) = self.cloud_service.get_user_service() { + if let Ok(user_service) = self.cloud_services.get_user_service() { user_service.receive_realtime_event(json) } } - #[instrument(level = "info", skip_all)] pub(crate) async fn generate_sign_in_url_with_email( &self, - authenticator: &AuthType, + authenticator: &Authenticator, email: &str, ) -> Result { - self - .cloud_service - .set_server_auth_type(authenticator, None)?; + self.cloud_services.set_user_authenticator(authenticator); - let auth_service = self.cloud_service.get_user_service()?; + let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; Ok(url) } - #[instrument(level = "info", skip_all)] - pub(crate) async fn sign_in_with_password( - &self, - email: &str, - password: &str, - ) -> Result { - self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; - let response = auth_service.sign_in_with_password(email, password).await?; - Ok(response) - } - - #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_magic_link( &self, email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) .await?; Ok(()) } - #[instrument(level = "info", skip_all)] - pub(crate) async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result { - self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; - let response = auth_service.sign_in_with_passcode(email, passcode).await?; - Ok(response) - } - - #[instrument(level = "info", skip_all)] pub(crate) async fn generate_oauth_url( &self, oauth_provider: &str, ) -> Result { self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) .await?; @@ -722,29 +753,27 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - auth_type: AuthType, + authenticator: &Authenticator, session: &Session, ) -> Result<(), FlowyError> { - let user_profile = UserProfile::from((response, &auth_type)); + let user_profile = UserProfile::from((response, authenticator)); let uid = user_profile.uid; - - if auth_type.is_local() { + if authenticator.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); self.set_anon_user(session); } - let mut conn = self.db_connection(uid)?; - sync_user_workspaces_with_diff(uid, auth_type, response.user_workspaces(), &mut conn)?; + save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; info!( "Save new user profile to disk, authenticator: {:?}", - auth_type + authenticator ); self .authenticate_user .set_session(Some(session.clone().into()))?; self - .save_user(uid, (user_profile, auth_type).into()) + .save_user(uid, (user_profile, authenticator.clone()).into()) .await?; Ok(()) } @@ -753,10 +782,14 @@ impl UserManager { let session = self.get_session()?; if session.user_id == user_update.uid { debug!("Receive user update: {:?}", user_update); + let user_profile = self.get_user_profile_from_disk(user_update.uid).await?; + if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) { + return Ok(()); + } + // Save the user profile change upsert_user_profile_change( user_update.uid, - &session.user_workspace.id, self.db_connection(user_update.uid)?, UserTableChangeset::from(user_update), )?; @@ -769,37 +802,41 @@ impl UserManager { &self, old_user: &AnonUser, _new_user_session: &Session, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; - if auth_type == &AuthType::AppFlowyCloud { + if authenticator == &Authenticator::AppFlowyCloud { self .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) .await?; } // Save the old user workspace setting. - let mut conn = self - .authenticate_user - .database - .get_connection(old_user.session.user_id)?; - upsert_user_workspace( + save_user_workspace( old_user.session.user_id, - *auth_type, - old_user.session.user_workspace.clone(), - &mut conn, + self + .authenticate_user + .database + .get_connection(old_user.session.user_id)?, + &old_user.session.user_workspace.clone(), )?; Ok(()) } } +fn current_authenticator() -> Authenticator { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => Authenticator::Local, + AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud, + } +} + pub fn upsert_user_profile_change( uid: i64, - workspace_id: &str, mut conn: DBConnection, changeset: UserTableChangeset, ) -> FlowyResult<()> { @@ -808,8 +845,11 @@ pub fn upsert_user_profile_change( "Update user profile with changeset: {:?}", changeset ); - update_user_profile(&mut conn, changeset)?; - let user = select_user_profile(uid, workspace_id, &mut conn)?; + diesel_update_table!(user_table, changeset, &mut *conn); + let user: UserProfile = user_table::dsl::user_table + .filter(user_table::id.eq(&uid.to_string())) + .first::(&mut *conn)? + .into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile) .payload(UserProfilePB::from(user)) .send(); @@ -817,15 +857,10 @@ pub fn upsert_user_profile_change( } #[instrument(level = "info", skip_all, err)] -fn save_user_token( - uid: i64, - workspace_id: &str, - conn: DBConnection, - token: String, -) -> FlowyResult<()> { +fn save_user_token(uid: i64, conn: DBConnection, token: String) -> FlowyResult<()> { let params = UpdateUserProfileParams::new(uid).with_token(token); let changeset = UserTableChangeset::new(params); - upsert_user_profile_change(uid, workspace_id, conn, changeset) + upsert_user_profile_change(uid, conn, changeset) } #[instrument(level = "info", skip_all, err)] @@ -844,7 +879,6 @@ fn collab_migration_list() -> Vec> { Box::new(FavoriteV1AndWorkspaceArrayMigration), Box::new(WorkspaceTrashMapToSectionMigration), Box::new(CollabDocKeyWithWorkspaceIdMigration), - Box::new(AnonUserWorkspaceTableMigration), ] } @@ -857,9 +891,9 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc) { } } -pub(crate) fn run_data_migration( +pub(crate) fn run_collab_data_migration( session: &Session, - user_auth_type: &AuthType, + user: &UserProfile, collab_db: Arc, sqlite_pool: Arc, kv: Arc, @@ -868,7 +902,7 @@ pub(crate) fn run_data_migration( let migrations = collab_migration_list(); match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( migrations, - user_auth_type, + &user.authenticator, app_version, ) { Ok(applied_migrations) => { @@ -883,7 +917,6 @@ pub(crate) fn run_data_migration( } } -#[instrument(level = "info", skip_all, err)] pub async fn sign_out( cloud_services: &Arc, session: &Session, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index 188cc3c5ac..8d20bae427 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -4,15 +4,18 @@ use tracing::instrument; use crate::entities::UserProfilePB; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; -pub const ANON_USER: &str = "anon_user"; +const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] - pub async fn get_migration_user(&self, current_authenticator: &AuthType) -> Option { + pub async fn get_migration_user( + &self, + current_authenticator: &Authenticator, + ) -> Option { // No need to migrate if the user is already local if current_authenticator.is_local() { return None; @@ -20,11 +23,11 @@ impl UserManager { let session = self.get_session().ok()?; let user_profile = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .get_user_profile_from_disk(session.user_id) .await .ok()?; - if user_profile.auth_type.is_local() { + if user_profile.authenticator.is_local() { Some(AnonUser { session }) } else { None @@ -48,23 +51,11 @@ impl UserManager { "Anon user not found", ))?; let profile = self - .get_user_profile_from_disk(anon_session.user_id, &anon_session.user_workspace.id) + .get_user_profile_from_disk(anon_session.user_id) .await?; Ok(UserProfilePB::from(profile)) } - pub fn get_anon_user_id(&self) -> FlowyResult { - let anon_session = self - .store_preferences - .get_object::(ANON_USER) - .ok_or(FlowyError::new( - ErrorCode::RecordNotFound, - "Anon user not found", - ))?; - - Ok(anon_session.user_id) - } - /// Opens a historical user's session based on their user ID, device ID, and authentication type. /// /// This function facilitates the re-opening of a user's session from historical tracking. diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index 47826054bf..d8c749ac0c 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -8,13 +8,13 @@ use collab_entity::CollabType; use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, }; -use collab_integrate::CollabKVDB; use collab_user::core::{UserAwareness, UserAwarenessNotifier}; use dashmap::try_result::TryResult; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; use tracing::{error, info, instrument, trace}; -use uuid::Uuid; + +use collab_integrate::CollabKVDB; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; use crate::entities::ReminderPB; use crate::user_manager::UserManager; @@ -119,9 +119,11 @@ impl UserManager { pub(crate) async fn initial_user_awareness( &self, session: &Session, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { - let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); + let authenticator = authenticator.clone(); + let object_id = + user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); // Try to acquire mutable access to `is_loading_awareness`. // Thread-safety is ensured by DashMap @@ -154,21 +156,23 @@ impl UserManager { let is_exist_on_disk = self .authenticate_user - .is_collab_on_disk(session.user_id, &object_id.to_string())?; - if auth_type.is_local() || is_exist_on_disk { + .is_collab_on_disk(session.user_id, &object_id)?; + if authenticator.is_local() || is_exist_on_disk { trace!( "Initializing new user awareness from disk:{}, {:?}", object_id, - auth_type + authenticator ); let collab_db = self.get_collab_db(session.user_id)?; - let workspace_id = session.user_workspace.workspace_id()?; - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); + let doc_state = CollabPersistenceImpl::new( + collab_db.clone(), + session.user_id, + session.user_workspace.id.clone(), + ) + .into_data_source(); let awareness = Self::collab_for_user_awareness( &self.collab_builder.clone(), - &workspace_id, + &session.user_workspace.id, session.user_id, &object_id, collab_db, @@ -184,9 +188,9 @@ impl UserManager { } else { info!( "Initializing new user awareness from server:{}, {:?}", - object_id, auth_type + object_id, authenticator ); - self.load_awareness_from_server(session, object_id, *auth_type)?; + self.load_awareness_from_server(session, object_id, authenticator.clone())?; } } else { return Err(FlowyError::new( @@ -207,15 +211,15 @@ impl UserManager { fn load_awareness_from_server( &self, session: &Session, - object_id: Uuid, - authenticator: AuthType, + object_id: String, + authenticator: Authenticator, ) -> FlowyResult<()> { // Clone necessary data let session = session.clone(); let collab_db = self.get_collab_db(session.user_id)?; let weak_builder = self.collab_builder.clone(); let user_awareness = Arc::downgrade(&self.user_awareness); - let cloud_services = self.cloud_service.clone(); + let cloud_services = self.cloud_services.clone(); let authenticate_user = self.authenticate_user.clone(); let is_loading_awareness = self.is_loading_awareness.clone(); @@ -227,14 +231,16 @@ impl UserManager { } }; - let workspace_id = session.user_workspace.workspace_id()?; let create_awareness = if authenticator.is_local() { - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); + let doc_state = CollabPersistenceImpl::new( + collab_db.clone(), + session.user_id, + session.user_workspace.id.clone(), + ) + .into_data_source(); Self::collab_for_user_awareness( &weak_builder, - &workspace_id, + &session.user_workspace.id, session.user_id, &object_id, collab_db, @@ -245,7 +251,7 @@ impl UserManager { } else { let result = cloud_services .get_user_service()? - .get_user_awareness_doc_state(session.user_id, &workspace_id, &object_id) + .get_user_awareness_doc_state(session.user_id, &session.user_workspace.id, &object_id) .await; match result { @@ -253,7 +259,7 @@ impl UserManager { trace!("Fetched user awareness collab from remote: {}", data.len()); Self::collab_for_user_awareness( &weak_builder, - &workspace_id, + &session.user_workspace.id, session.user_id, &object_id, collab_db, @@ -265,12 +271,15 @@ impl UserManager { Err(err) => { if err.is_record_not_found() { info!("User awareness not found, creating new"); - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); + let doc_state = CollabPersistenceImpl::new( + collab_db.clone(), + session.user_id, + session.user_workspace.id.clone(), + ) + .into_data_source(); Self::collab_for_user_awareness( &weak_builder, - &workspace_id, + &session.user_workspace.id, session.user_id, &object_id, collab_db, @@ -320,9 +329,9 @@ impl UserManager { /// user awareness. async fn collab_for_user_awareness( collab_builder: &Weak, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, - object_id: &Uuid, + object_id: &str, collab_db: Weak, doc_state: DataSource, notifier: Option, @@ -366,7 +375,8 @@ impl UserManager { info!("User awareness is not loaded when trying to access it"); let session = self.get_session()?; - let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); + let object_id = + user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); let is_loading = self .is_loading_awareness .get(&object_id) @@ -374,11 +384,9 @@ impl UserManager { .unwrap_or(false); if !is_loading { - let user_profile = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; + let user_profile = self.get_user_profile_from_disk(session.user_id).await?; self - .initial_user_awareness(&session, &user_profile.workspace_auth_type) + .initial_user_awareness(&session, &user_profile.authenticator) .await?; } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs index 1462d1f019..2bfba3422e 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs @@ -1,9 +1,31 @@ +use crate::entities::{AuthStateChangedPB, AuthStatePB}; +use crate::notification::send_auth_state_notification; use crate::services::cloud_config::get_encrypt_secret; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::entities::{ + EncryptionType, UpdateUserProfileParams, UserCredentials, UserProfile, +}; use lib_infra::encryption::{decrypt_text, encrypt_text}; impl UserManager { + pub async fn set_encrypt_secret( + &self, + uid: i64, + secret: String, + encryption_type: EncryptionType, + ) -> FlowyResult<()> { + let params = UpdateUserProfileParams::new(uid).with_encryption_type(encryption_type); + self + .cloud_services + .get_user_service()? + .update_user(UserCredentials::from_uid(uid), params.clone()) + .await?; + self.cloud_services.set_encrypt_secret(secret); + + Ok(()) + } + pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult { let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?; Ok(encrypt_sign) @@ -41,3 +63,16 @@ impl UserManager { } } } + +pub(crate) fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { + // If the local user profile's encryption sign is not equal to the user update's encryption sign, + // which means the user enable encryption in another device, we should logout the current user. + let is_valid = user_profile.encryption_type.sign() == encryption_sign; + if !is_valid { + send_auth_state_notification(AuthStateChangedPB { + state: AuthStatePB::InvalidAuth, + message: "Encryption configuration was changed".to_string(), + }); + } + is_valid +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index b78d635133..e666d2486a 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -2,12 +2,25 @@ use chrono::{Duration, NaiveDateTime, Utc}; use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlanDetail}; use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; -use std::str::FromStr; +use std::convert::TryFrom; use std::sync::Arc; +use collab_entity::{CollabObject, CollabType}; +use collab_integrate::CollabKVDB; +use tracing::{error, info, instrument, trace, warn}; + +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +use flowy_user_pub::entities::{ + Role, UpdateUserProfileParams, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, + WorkspaceMember, +}; + use crate::entities::{ - RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, - UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, + RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, + UpdateUserWorkspaceSettingPB, UseAISettingPB, UserWorkspacePB, WorkspaceSubscriptionInfoPB, }; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; @@ -15,20 +28,16 @@ use crate::services::billing_check::PeriodicallyCheckBillingState; use crate::services::data_import::{ generate_import_data, upload_collab_objects_data, ImportedFolder, ImportedSource, }; - -use crate::user_manager::UserManager; -use collab_integrate::CollabKVDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::ConnectionPool; -use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; -use flowy_user_pub::entities::{ - AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +use crate::services::sqlite_sql::member_sql::{ + select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, }; +use crate::services::sqlite_sql::user_sql::UserTableChangeset; +use crate::services::sqlite_sql::workspace_sql::{ + get_all_user_workspace_op, get_user_workspace_op, insert_or_update_workspaces_op, + UserWorkspaceTable, +}; +use crate::user_manager::{upsert_user_profile_change, UserManager}; use flowy_user_pub::session::Session; -use flowy_user_pub::sql::*; -use tracing::{error, info, instrument, trace}; -use uuid::Uuid; impl UserManager { /// Import appflowy data from the given path. @@ -101,7 +110,7 @@ impl UserManager { collab_data: ImportedCollabData, ) -> Result<(), FlowyError> { let user = self - .get_user_profile_from_disk(current_session.user_id, ¤t_session.user_workspace.id) + .get_user_profile_from_disk(current_session.user_id) .await?; let user_collab_db = self .get_collab_db(current_session.user_id)? @@ -110,12 +119,12 @@ impl UserManager { let user_id = current_session.user_id; let weak_user_collab_db = Arc::downgrade(&user_collab_db); - let weak_user_cloud_service = self.cloud_service.get_user_service()?; + let weak_user_cloud_service = self.cloud_services.get_user_service()?; match upload_collab_objects_data( user_id, weak_user_collab_db, - ¤t_session.user_workspace.workspace_id()?, - &user.workspace_auth_type, + ¤t_session.user_workspace.id, + &user.authenticator, collab_data, weak_user_cloud_service, ) @@ -152,65 +161,23 @@ impl UserManager { } #[instrument(skip(self), err)] - pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { - info!("open workspace: {}, auth type:{}", workspace_id, auth_type); - let workspace_id_str = workspace_id.to_string(); - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; - - let uid = self.user_id()?; - let profile = self - .get_user_profile_from_disk(uid, &workspace_id_str) + pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + info!("open workspace: {}", workspace_id); + let user_workspace = self + .cloud_services + .get_user_service()? + .open_workspace(workspace_id) .await?; - if let Err(err) = self.cloud_service.set_token(&profile.token) { - error!("Set token failed: {}", err); - } - - let mut conn = self.db_connection(self.user_id()?)?; - let user_workspace = match select_user_workspace(&workspace_id_str, &mut conn) { - Err(err) => { - if err.is_record_not_found() { - sync_workspace( - workspace_id, - self.cloud_service.get_user_service()?, - uid, - auth_type, - self.db_pool(uid)?, - ) - .await? - } else { - return Err(err); - } - }, - Ok(row) => { - let user_workspace = UserWorkspace::from(row); - let workspace_id = *workspace_id; - let user_service = self.cloud_service.get_user_service()?; - let pool = self.db_pool(uid)?; - tokio::spawn(async move { - let _ = sync_workspace(&workspace_id, user_service, uid, auth_type, pool).await; - }); - user_workspace - }, - }; self .authenticate_user .set_user_workspace(user_workspace.clone())?; let uid = self.user_id()?; - if let Err(err) = self - .user_status_callback - .read() - .await - .on_workspace_opened(uid, workspace_id, &user_workspace, &auth_type) - .await - { - error!("Open workspace failed: {:?}", err); - } + let user_profile = self.get_user_profile_from_disk(uid).await?; if let Err(err) = self - .initial_user_awareness(self.get_session()?.as_ref(), &auth_type) + .initial_user_awareness(self.get_session()?.as_ref(), &user_profile.authenticator) .await { error!( @@ -219,54 +186,74 @@ impl UserManager { ); } + if let Err(err) = self + .user_status_callback + .read() + .await + .open_workspace(uid, &user_workspace, &user_profile.authenticator) + .await + { + error!("Open workspace failed: {:?}", err); + } + Ok(()) } #[instrument(level = "info", skip(self), err)] - pub async fn create_workspace( - &self, - workspace_name: &str, - auth_type: AuthType, - ) -> FlowyResult { - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; - + pub async fn add_workspace(&self, workspace_name: &str) -> FlowyResult { let new_workspace = self - .cloud_service + .cloud_services .get_user_service()? .create_workspace(workspace_name) .await?; info!( - "create workspace: {}, name:{}, auth_type: {}", - new_workspace.id, new_workspace.name, auth_type + "new workspace: {}, name:{}", + new_workspace.id, new_workspace.name ); // save the workspace to sqlite db let uid = self.user_id()?; let mut conn = self.db_connection(uid)?; - upsert_user_workspace(uid, auth_type, new_workspace.clone(), &mut conn)?; + insert_or_update_workspaces_op(uid, &[new_workspace.clone()], &mut conn)?; Ok(new_workspace) } pub async fn patch_workspace( &self, - workspace_id: &Uuid, - changeset: UserWorkspaceChangeset, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? - .patch_workspace(workspace_id, changeset.name.clone(), changeset.icon.clone()) + .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) .await?; // save the icon and name to sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - update_user_workspace(conn, changeset)?; + let mut user_workspace = match self.get_user_workspace(uid, workspace_id) { + Some(user_workspace) => user_workspace, + None => { + return Err(FlowyError::record_not_found().with_context(format!( + "Expected to find user workspace with id: {}, but not found", + workspace_id + ))); + }, + }; - let row = self.get_user_workspace_from_db(uid, workspace_id)?; - let payload = UserWorkspacePB::from(row); + if let Some(new_workspace_name) = new_workspace_name { + user_workspace.name = new_workspace_name.to_string(); + } + if let Some(new_workspace_icon) = new_workspace_icon { + user_workspace.icon = new_workspace_icon.to_string(); + } + + let _ = save_user_workspace(uid, conn, &user_workspace); + + let payload: UserWorkspacePB = user_workspace.clone().into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) .send(); @@ -275,10 +262,10 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn leave_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub async fn leave_workspace(&self, workspace_id: &str) -> FlowyResult<()> { info!("leave workspace: {}", workspace_id); self - .cloud_service + .cloud_services .get_user_service()? .leave_workspace(workspace_id) .await?; @@ -286,42 +273,40 @@ impl UserManager { // delete workspace from local sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspace(conn, workspace_id.to_string().as_str())?; + delete_user_workspaces(conn, workspace_id)?; self .user_workspace_service - .did_delete_workspace(workspace_id) - .await + .did_delete_workspace(workspace_id.to_string()) } #[instrument(level = "info", skip(self), err)] - pub async fn delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { info!("delete workspace: {}", workspace_id); self - .cloud_service + .cloud_services .get_user_service()? .delete_workspace(workspace_id) .await?; let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspace(conn, workspace_id.to_string().as_str())?; + delete_user_workspaces(conn, workspace_id)?; self .user_workspace_service - .did_delete_workspace(workspace_id) - .await?; + .did_delete_workspace(workspace_id.to_string())?; Ok(()) } pub async fn invite_member_to_workspace( &self, - workspace_id: Uuid, + workspace_id: String, invitee_email: String, role: Role, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .invite_workspace_member(invitee_email, workspace_id, role) .await?; @@ -331,7 +316,7 @@ impl UserManager { pub async fn list_pending_workspace_invitations(&self) -> FlowyResult> { let status = Some(WorkspaceInvitationStatus::Pending); let invitations = self - .cloud_service + .cloud_services .get_user_service()? .list_workspace_invitations(status) .await?; @@ -340,7 +325,7 @@ impl UserManager { pub async fn accept_workspace_invitation(&self, invite_id: String) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .accept_workspace_invitations(invite_id) .await?; @@ -350,10 +335,10 @@ impl UserManager { pub async fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .remove_workspace_member(user_email, workspace_id) .await?; @@ -362,10 +347,10 @@ impl UserManager { pub async fn get_workspace_members( &self, - workspace_id: Uuid, + workspace_id: String, ) -> FlowyResult> { let members = self - .cloud_service + .cloud_services .get_user_service()? .get_workspace_members(workspace_id) .await?; @@ -374,13 +359,13 @@ impl UserManager { pub async fn get_workspace_member( &self, - workspace_id: Uuid, + workspace_id: String, uid: i64, ) -> FlowyResult { let member = self - .cloud_service + .cloud_services .get_user_service()? - .get_workspace_member(&workspace_id, uid) + .get_workspace_member(workspace_id, uid) .await?; Ok(member) } @@ -388,84 +373,60 @@ impl UserManager { pub async fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .update_workspace_member(user_email, workspace_id, role) .await?; Ok(()) } - pub fn get_user_workspace_from_db( - &self, - uid: i64, - workspace_id: &Uuid, - ) -> FlowyResult { - let mut conn = self.db_connection(uid)?; - select_user_workspace(workspace_id.to_string().as_str(), &mut conn) + pub fn get_user_workspace(&self, uid: i64, workspace_id: &str) -> Option { + let conn = self.db_connection(uid).ok()?; + get_user_workspace_op(workspace_id, conn) } - pub async fn get_all_user_workspaces( - &self, - uid: i64, - auth_type: AuthType, - ) -> FlowyResult> { - // 1) Load & return the local copy immediately - let mut conn = self.db_connection(uid)?; - let local_workspaces = select_all_user_workspace(uid, &mut conn)?; + pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { + let conn = self.db_connection(uid)?; + let workspaces = get_all_user_workspace_op(uid, conn)?; - // 2) If both cloud service and pool are available, fire off a background sync - if let (Ok(service), Ok(pool)) = (self.cloud_service.get_user_service(), self.db_pool(uid)) { - // capture only what we need - let auth_copy = auth_type; - - tokio::spawn(async move { - // fetch remote list - let new_ws = match service.get_all_workspace(uid).await { - Ok(ws) => ws, - Err(e) => { - trace!("failed to fetch remote workspaces for {}: {:?}", uid, e); - return; - }, - }; - - // get a pooled DB connection - let mut conn = match pool.get() { - Ok(c) => c, - Err(e) => { - trace!("failed to get DB connection for {}: {:?}", uid, e); - return; - }, - }; - - // sync + diff - match sync_user_workspaces_with_diff(uid, auth_copy, &new_ws, &mut conn) { - Ok(changes) if !changes.is_empty() => { - info!( - "synced {} workspaces for user {} and auth type {:?}. changes: {:?}", - changes.len(), - uid, - auth_copy, - changes - ); - // only send notification if there were real changes - if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) { - let repeated_pb = RepeatedUserWorkspacePB::from(updated_list); + if let Ok(service) = self.cloud_services.get_user_service() { + if let Ok(pool) = self.db_pool(uid) { + tokio::spawn(async move { + if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { + if let Ok(conn) = pool.get() { + let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces); + let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) - .payload(repeated_pb) + .payload(repeated_workspace_pbs) .send(); } - }, - Ok(_) => trace!("no workspaces updated for {}", uid), - Err(e) => trace!("sync error for {}: {:?}", uid, e), - } - }); + } + }); + } } + Ok(workspaces) + } - Ok(local_workspaces) + /// Reset the remote workspace using local workspace data. This is useful when a user wishes to + /// open a workspace on a new device that hasn't fully synchronized with the server. + pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> { + let collab_object = CollabObject::new( + reset.uid, + reset.workspace_id.clone(), + CollabType::Folder, + reset.workspace_id.clone(), + self.authenticate_user.user_config.device_id.clone(), + ); + self + .cloud_services + .get_user_service()? + .reset_workspace(collab_object) + .await?; + Ok(()) } #[instrument(level = "info", skip(self), err)] @@ -473,12 +434,11 @@ impl UserManager { &self, workspace_subscription: SubscribeWorkspacePB, ) -> FlowyResult { - let workspace_id = Uuid::from_str(&workspace_subscription.workspace_id)?; let payment_link = self - .cloud_service + .cloud_services .get_user_service()? .subscribe_workspace( - workspace_id, + workspace_subscription.workspace_id, workspace_subscription.recurring_interval.into(), workspace_subscription.workspace_subscription_plan.into(), workspace_subscription.success_url, @@ -493,11 +453,10 @@ impl UserManager { &self, workspace_id: String, ) -> FlowyResult { - let workspace_id = Uuid::from_str(&workspace_id)?; let subscriptions = self - .cloud_service + .cloud_services .get_user_service()? - .get_workspace_subscription_one(&workspace_id) + .get_workspace_subscription_one(workspace_id.clone()) .await?; Ok(WorkspaceSubscriptionInfoPB::from(subscriptions)) @@ -511,7 +470,7 @@ impl UserManager { reason: Option, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .cancel_workspace_subscription(workspace_id, plan, reason) .await?; @@ -521,12 +480,12 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn update_workspace_subscription_payment_period( &self, - workspace_id: &Uuid, + workspace_id: String, plan: SubscriptionPlan, recurring_interval: RecurringInterval, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .update_workspace_subscription_payment_period(workspace_id, plan, recurring_interval) .await?; @@ -536,7 +495,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_subscription_plan_details(&self) -> FlowyResult> { let plan_details = self - .cloud_service + .cloud_services .get_user_service()? .get_subscription_plan_details() .await?; @@ -546,10 +505,10 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_workspace_usage( &self, - workspace_id: &Uuid, + workspace_id: String, ) -> FlowyResult { let workspace_usage = self - .cloud_service + .cloud_services .get_user_service()? .get_workspace_usage(workspace_id) .await?; @@ -567,7 +526,7 @@ impl UserManager { .user_status_callback .read() .await - .on_storage_permission_updated(can_write); + .did_update_storage_limitation(can_write); Ok(workspace_usage) } @@ -575,7 +534,7 @@ impl UserManager { #[instrument(level = "info", skip(self), err)] pub async fn get_billing_portal_url(&self) -> FlowyResult { let url = self - .cloud_service + .cloud_services .get_user_service()? .get_billing_portal_url() .await?; @@ -586,83 +545,49 @@ impl UserManager { &self, updated_settings: UpdateUserWorkspaceSettingPB, ) -> FlowyResult<()> { - let workspace_id = Uuid::from_str(&updated_settings.workspace_id)?; - let cloud_service = self.cloud_service.get_user_service()?; + let ai_model = updated_settings.ai_model.clone(); + let workspace_id = updated_settings.workspace_id.clone(); + let cloud_service = self.cloud_services.get_user_service()?; let settings = cloud_service - .update_workspace_setting(&workspace_id, updated_settings.clone().into()) + .update_workspace_setting(&workspace_id, updated_settings.into()) .await?; - let changeset = WorkspaceSettingsChangeset { - id: workspace_id.to_string(), - disable_search_indexing: updated_settings.disable_search_indexing, - ai_model: updated_settings.ai_model.clone(), - }; - + let pb = UseAISettingPB::from(settings); let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - update_workspace_setting(&mut conn, changeset)?; + send_notification(&uid.to_string(), UserNotification::DidUpdateAISetting) + .payload(pb) + .send(); - let pb = WorkspaceSettingsPB::from(&settings); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(pb) - .send(); + if let Some(ai_model) = &ai_model { + if let Err(err) = self.cloud_services.set_ai_model(ai_model) { + error!("Set ai model failed: {}", err); + } + + let conn = self.db_connection(uid)?; + let params = UpdateUserProfileParams::new(uid).with_ai_model(ai_model); + upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; + } Ok(()) } - pub async fn get_workspace_settings( - &self, - workspace_id: &Uuid, - ) -> FlowyResult { + pub async fn get_workspace_settings(&self, workspace_id: &str) -> FlowyResult { + let cloud_service = self.cloud_services.get_user_service()?; + let settings = cloud_service.get_workspace_setting(workspace_id).await?; let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - match select_workspace_setting(&mut conn, &workspace_id.to_string()) { - Ok(workspace_settings) => { - trace!("workspace settings found in local db"); - let pb = WorkspaceSettingsPB::from(workspace_settings); - let old_pb = pb.clone(); - let workspace_id = *workspace_id; - - // Spawn a task to sync remote settings using the helper - let pool = self.db_pool(uid)?; - let cloud_service = self.cloud_service.clone(); - tokio::spawn(async move { - let _ = sync_workspace_settings(cloud_service, workspace_id, old_pb, uid, pool).await; - }); - Ok(pb) - }, - Err(err) => { - if err.is_record_not_found() { - trace!("No workspace settings found, fetch from remote"); - let service = self.cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(workspace_id).await?; - let pb = WorkspaceSettingsPB::from(&settings); - let mut conn = self.db_connection(uid)?; - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(workspace_id, &settings), - )?; - Ok(pb) - } else { - Err(err) - } - }, - } + let conn = self.db_connection(uid)?; + let params = UpdateUserProfileParams::new(uid).with_ai_model(&settings.ai_model); + upsert_user_profile_change(uid, conn, UserTableChangeset::new(params))?; + Ok(UseAISettingPB::from(settings)) } - pub async fn get_workspace_member_info( - &self, - uid: i64, - workspace_id: &Uuid, - ) -> FlowyResult { + pub async fn get_workspace_member_info(&self, uid: i64) -> FlowyResult { + let workspace_id = self.get_session()?.user_workspace.id.clone(); let db = self.authenticate_user.get_sqlite_connection(uid)?; // Can opt in using memory cache - if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { + if let Ok(member_record) = select_workspace_member(db, &workspace_id, uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self - .get_workspace_member_info_from_remote(workspace_id, uid) + .get_workspace_member_info_from_remote(&workspace_id, uid) .await?; } @@ -675,7 +600,7 @@ impl UserManager { } let member = self - .get_workspace_member_info_from_remote(workspace_id, uid) + .get_workspace_member_info_from_remote(&workspace_id, uid) .await?; Ok(member) @@ -683,19 +608,19 @@ impl UserManager { async fn get_workspace_member_info_from_remote( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, ) -> FlowyResult { trace!("get workspace member info from remote: {}", workspace_id); let member = self - .cloud_service + .cloud_services .get_user_service()? - .get_workspace_member(workspace_id, uid) + .get_workspace_member_info(workspace_id, uid) .await?; let record = WorkspaceMemberTable { email: member.email.clone(), - role: member.role.into(), + role: member.role.clone().into(), name: member.name.clone(), avatar_url: member.avatar_url.clone(), uid, @@ -703,8 +628,8 @@ impl UserManager { updated_at: Utc::now().naive_utc(), }; - let mut db = self.authenticate_user.get_sqlite_connection(uid)?; - upsert_workspace_member(&mut db, record)?; + let db = self.authenticate_user.get_sqlite_connection(uid)?; + upsert_workspace_member(db, record)?; Ok(member) } @@ -713,11 +638,10 @@ impl UserManager { success: SuccessWorkspaceSubscriptionPB, ) -> FlowyResult<()> { // periodically check the billing state - let workspace_id = Uuid::from_str(&success.workspace_id)?; let plans = PeriodicallyCheckBillingState::new( - workspace_id, + success.workspace_id, success.plan.map(SubscriptionPlan::from), - Arc::downgrade(&self.cloud_service), + Arc::downgrade(&self.cloud_services), Arc::downgrade(&self.authenticate_user), ) .start() @@ -728,11 +652,122 @@ impl UserManager { .user_status_callback .read() .await - .on_subscription_plans_updated(plans); + .did_update_plans(plans); Ok(()) } } +/// This method is used to save one user workspace to the SQLite database +/// +/// If the workspace is already persisted in the database, it will be overridden. +/// +/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user. +/// +pub fn save_user_workspace( + uid: i64, + mut conn: DBConnection, + user_workspace: &UserWorkspace, +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?; + let affected_rows = diesel::update( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(&user_workspace.id)), + ) + .set(( + user_workspace_table::name.eq(&user_workspace.name), + user_workspace_table::created_at.eq(&user_workspace.created_at), + user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), + user_workspace_table::icon.eq(&user_workspace.icon), + user_workspace_table::member_count.eq(&user_workspace.member_count), + )) + .execute(conn)?; + + if affected_rows == 0 { + diesel::insert_into(user_workspace_table::table) + .values(user_workspace) + .execute(conn)?; + } + + Ok::<(), FlowyError>(()) + }) +} + +/// This method is used to save the user workspaces (plural) to the SQLite database +/// +/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. +/// +/// Consider using [save_user_workspace] if you only need to save a single workspace. +/// +pub fn save_all_user_workspaces( + uid: i64, + mut conn: DBConnection, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + let user_workspaces = user_workspaces + .iter() + .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace))) + .collect::, _>>()?; + + conn.immediate_transaction(|conn| { + let existing_ids = user_workspace_table::dsl::user_workspace_table + .select(user_workspace_table::id) + .load::(conn)?; + let new_ids: Vec = user_workspaces.iter().map(|w| w.id.clone()).collect(); + let ids_to_delete: Vec = existing_ids + .into_iter() + .filter(|id| !new_ids.contains(id)) + .collect(); + + // insert or update the user workspaces + for user_workspace in &user_workspaces { + let affected_rows = diesel::update( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(&user_workspace.id)), + ) + .set(( + user_workspace_table::name.eq(&user_workspace.name), + user_workspace_table::created_at.eq(&user_workspace.created_at), + user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), + user_workspace_table::icon.eq(&user_workspace.icon), + user_workspace_table::member_count.eq(&user_workspace.member_count), + user_workspace_table::role.eq(&user_workspace.role), + )) + .execute(conn)?; + + if affected_rows == 0 { + diesel::insert_into(user_workspace_table::table) + .values(user_workspace) + .execute(conn)?; + } + } + + // delete the user workspaces that are not in the new list + if !ids_to_delete.is_empty() { + diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq_any(ids_to_delete)), + ) + .execute(conn)?; + } + + Ok::<(), FlowyError>(()) + }) +} + +pub fn delete_user_workspaces(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::(rows_affected) + })?; + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); + } + Ok(()) +} + fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { let current_time: NaiveDateTime = Utc::now().naive_utc(); match current_time.checked_sub_signed(Duration::minutes(minutes)) { @@ -740,45 +775,3 @@ fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { None => false, } } - -async fn sync_workspace_settings( - cloud_service: Arc, - workspace_id: Uuid, - old_pb: WorkspaceSettingsPB, - uid: i64, - pool: Arc, -) -> FlowyResult<()> { - let service = cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(&workspace_id).await?; - let new_pb = WorkspaceSettingsPB::from(&settings); - if new_pb != old_pb { - trace!("workspace settings updated"); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(new_pb) - .send(); - if let Ok(mut conn) = pool.get() { - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), - )?; - } - } - Ok(()) -} - -async fn sync_workspace( - workspace_id: &Uuid, - user_service: Arc, - uid: i64, - auth_type: AuthType, - pool: Arc, -) -> FlowyResult { - let user_workspace = user_service.open_workspace(workspace_id).await?; - if let Ok(mut conn) = pool.get() { - upsert_user_workspace(uid, auth_type, user_workspace.clone(), &mut conn)?; - } - Ok(user_workspace) -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs index 23c050c1f2..3ce66227c5 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs @@ -3,5 +3,6 @@ pub(crate) mod manager_history_user; pub(crate) mod manager_user_awareness; pub(crate) mod manager_user_encryption; pub(crate) mod manager_user_workspace; +mod user_login_state; pub use manager::*; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs new file mode 100644 index 0000000000..906002ad10 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs @@ -0,0 +1,11 @@ +use crate::migrations::AnonUser; +use flowy_user_pub::entities::{AuthResponse, Authenticator, UserProfile}; + +/// recording the intermediate state of the sign-in/sign-up process +#[derive(Clone)] +pub struct UserAuthProcess { + pub user_profile: UserProfile, + pub response: AuthResponse, + pub authenticator: Authenticator, + pub migration_user: Option, +} diff --git a/frontend/rust-lib/lib-infra/Cargo.toml b/frontend/rust-lib/lib-infra/Cargo.toml index a07a3413f4..12c805862b 100644 --- a/frontend/rust-lib/lib-infra/Cargo.toml +++ b/frontend/rust-lib/lib-infra/Cargo.toml @@ -22,7 +22,7 @@ validator = { workspace = true, features = ["derive"] } tracing.workspace = true atomic_refcell = "0.1" allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true } -futures = "0.3.31" +futures = "0.3.30" cfg-if = "1.0.0" futures-util = "0.3.30" @@ -36,7 +36,7 @@ base64 = { version = "0.22.1" } [dev-dependencies] rand = "0.8.5" -futures = "0.3.31" +futures = "0.3.30" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] zip = { version = "2.2.0", features = ["deflate"] } diff --git a/frontend/rust-lib/lib-infra/src/isolate_stream.rs b/frontend/rust-lib/lib-infra/src/isolate_stream.rs index cebc2b7d10..358214e985 100644 --- a/frontend/rust-lib/lib-infra/src/isolate_stream.rs +++ b/frontend/rust-lib/lib-infra/src/isolate_stream.rs @@ -7,7 +7,6 @@ use std::pin::Pin; use std::task::{Context, Poll}; #[pin_project] -#[derive(Clone, Debug)] pub struct IsolateSink { isolate: Isolate, } diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 216a01b232..45de7573d7 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -66,9 +66,9 @@ if [ "$exclude_packages" = false ]; then fi fi if [ "$verbose" = true ]; then - dart run build_runner build --delete-conflicting-outputs + dart run build_runner build else - dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 + dart run build_runner build >/dev/null 2>&1 fi echo "🧊 Done generating freezed files ($d)." fi @@ -108,9 +108,9 @@ fi # Start the build_runner in the background if [ "$verbose" = true ]; then - dart run build_runner build --delete-conflicting-outputs & + dart run build_runner build -d & else - dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 & + dart run build_runner build -d >/dev/null 2>&1 & fi # Get the PID of the background process diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index fd2edab785..e08bc873fd 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -64,7 +64,7 @@ cd .. cd freezed # Allow execution permissions on CI chmod +x ./generate_freezed.sh -./generate_freezed.sh "${args[@]}" --show-loading --verbose +./generate_freezed.sh "${args[@]}" --show-loading # Return to the original directory cd "$original_dir" diff --git a/frontend/scripts/tool/update_local_ai_rev.sh b/frontend/scripts/tool/update_local_ai_rev.sh index af24e0ba9f..83c5e67d80 100755 --- a/frontend/scripts/tool/update_local_ai_rev.sh +++ b/frontend/scripts/tool/update_local_ai_rev.sh @@ -15,7 +15,7 @@ for dir in "${directories[@]}"; do pushd "$dir" > /dev/null # Define the crates to update - crates=("af-local-ai" "af-plugin" "af-mcp") + crates=("appflowy-local-ai" "appflowy-plugin") for crate in "${crates[@]}"; do sed -i.bak "/^${crate}[[:alnum:]-]*[[:space:]]*=/s/rev = \"[a-fA-F0-9]\{6,40\}\"/rev = \"$NEW_REV\"/g" Cargo.toml diff --git a/frontend/scripts/white_label/code_white_label.sh b/frontend/scripts/white_label/code_white_label.sh deleted file mode 100644 index 1123a394ee..0000000000 --- a/frontend/scripts/white_label/code_white_label.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --company-name Set the custom company name" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --company-name \"MyCompany Ltd.\"" -} - -CUSTOM_COMPANY_NAME="" -CODE_FILE="appflowy_flutter/lib/workspace/application/notification/notification_service.dart" - -while [[ $# -gt 0 ]]; do - case $1 in - --company-name) - CUSTOM_COMPANY_NAME="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$CUSTOM_COMPANY_NAME" ]; then - echo "Error: Company name is required" - show_usage - exit 1 -fi - -if [ ! -f "$CODE_FILE" ]; then - echo "Error: Code file not found at $CODE_FILE" - exit 1 -fi - -echo "Replacing '_localNotifierAppName' value with '$CUSTOM_COMPANY_NAME' in code file..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -echo "Processing code file..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # First, escape any special characters in the company name - ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') - # Replace the _localNotifierAppName value with the custom company name - sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$ESCAPED_COMPANY_NAME'/" "$CODE_FILE" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $CODE_FILE with sed" - exit 1 - fi -else - # For Unix-like systems - sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$CUSTOM_COMPANY_NAME'/" "$CODE_FILE" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $CODE_FILE with sed" - exit 1 - fi -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/font_white_label.sh b/frontend/scripts/white_label/font_white_label.sh deleted file mode 100644 index 412ee6b062..0000000000 --- a/frontend/scripts/white_label/font_white_label.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" - echo " --font-family Set the name of the font family" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --font-path \"/path/to/fonts\" --font-family \"CustomFont\"" -} - -FONT_PATH="" -FONT_FAMILY="" -TARGET_FONT_DIR="appflowy_flutter/assets/fonts/" -PUBSPEC_FILE="appflowy_flutter/pubspec.yaml" -BASE_APPEARANCE_FILE="appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart" - -while [[ $# -gt 0 ]]; do - case $1 in - --font-path) - FONT_PATH="$2" - shift 2 - ;; - --font-family) - FONT_FAMILY="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -# Validate required arguments -if [ -z "$FONT_PATH" ]; then - echo "Error: Font path is required" - show_usage - exit 1 -fi - -if [ -z "$FONT_FAMILY" ]; then - echo "Error: Font family name is required" - show_usage - exit 1 -fi - -# Check if source directory exists -if [ ! -d "$FONT_PATH" ]; then - echo "Error: Font directory not found at $FONT_PATH" - exit 1 -fi - -# Create target directory if it doesn't exist -mkdir -p "$TARGET_FONT_DIR" - -# Clean existing fonts in target directory -echo "Cleaning existing fonts in $TARGET_FONT_DIR..." -rm -rf "$TARGET_FONT_DIR"/* - -# Copy font files to target directory -echo "Copying font files from $FONT_PATH to $TARGET_FONT_DIR..." -found_fonts=false -for ext in ttf otf; do - if ls "$FONT_PATH"/*."$ext" >/dev/null 2>&1; then - cp "$FONT_PATH"/*."$ext" "$TARGET_FONT_DIR"/ 2>/dev/null && found_fonts=true - fi -done - -if [ "$found_fonts" = false ]; then - echo "Error: No font files (.ttf or .otf) found in source directory" - exit 1 -fi - -# Generate font configuration for pubspec.yaml -echo "Generating font configuration..." - -# Create temporary file for font configuration -TEMP_FILE=$(mktemp) - -{ - echo " # BEGIN: WHITE_LABEL_FONT" - echo " - family: $FONT_FAMILY" - echo " fonts:" - - # Generate entries for each font file - for font_file in "$TARGET_FONT_DIR"/*; do - filename=$(basename "$font_file") - echo " - asset: assets/fonts/$filename" - - # Try to detect font weight from filename - if [[ $filename =~ (Thin|ExtraLight|Light|Regular|Medium|SemiBold|Bold|ExtraBold|Black) ]]; then - case ${BASH_REMATCH[1]} in - "Thin") echo " weight: 100";; - "ExtraLight") echo " weight: 200";; - "Light") echo " weight: 300";; - "Regular") echo " weight: 400";; - "Medium") echo " weight: 500";; - "SemiBold") echo " weight: 600";; - "Bold") echo " weight: 700";; - "ExtraBold") echo " weight: 800";; - "Black") echo " weight: 900";; - esac - fi - - # Try to detect italic style from filename - if [[ $filename =~ Italic ]]; then - echo " style: italic" - fi - done - echo " # END: WHITE_LABEL_FONT" -} > "$TEMP_FILE" - -# Update pubspec.yaml -echo "Updating pubspec.yaml..." -if [ -f "$PUBSPEC_FILE" ]; then - # Create a backup of the original file - cp "$PUBSPEC_FILE" "${PUBSPEC_FILE}.bak" - - if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Windows-specific handling - # First, remove existing white label font configuration - awk '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/{ next } /# White-label font configuration will be added here/{ print; system("cat '"$TEMP_FILE"'"); next } 1' "$PUBSPEC_FILE" > "${PUBSPEC_FILE}.tmp" - - if [ $? -eq 0 ]; then - mv "${PUBSPEC_FILE}.tmp" "$PUBSPEC_FILE" - rm -f "${PUBSPEC_FILE}.bak" - else - echo "Error: Failed to update pubspec.yaml" - mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" - rm -f "${PUBSPEC_FILE}.tmp" - rm -f "$TEMP_FILE" - exit 1 - fi - else - # Unix-like systems handling - if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" - else - SED_INPLACE="-i ''" - fi - - # Remove existing white label font configuration - sed $SED_INPLACE '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/d' "$PUBSPEC_FILE" - - # Add new font configuration - sed $SED_INPLACE "/# White-label font configuration will be added here/r $TEMP_FILE" "$PUBSPEC_FILE" - - if [ $? -ne 0 ]; then - echo "Error: Failed to update pubspec.yaml" - mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" - rm -f "$TEMP_FILE" - exit 1 - fi - rm -f "${PUBSPEC_FILE}.bak" - fi -else - echo "Error: pubspec.yaml not found at $PUBSPEC_FILE" - rm -f "$TEMP_FILE" - exit 1 -fi - -# Update base_appearance.dart -echo "Updating base_appearance.dart..." -if [ -f "$BASE_APPEARANCE_FILE" ]; then - # Create a backup of the original file - cp "$BASE_APPEARANCE_FILE" "${BASE_APPEARANCE_FILE}.bak" - - if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Windows-specific handling - sed -i "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" - else - # Unix-like systems handling - sed -i '' "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" - fi - - if [ $? -ne 0 ]; then - echo "Error: Failed to update base_appearance.dart" - mv "${BASE_APPEARANCE_FILE}.bak" "$BASE_APPEARANCE_FILE" - exit 1 - fi - rm -f "${BASE_APPEARANCE_FILE}.bak" -else - echo "Error: base_appearance.dart not found at $BASE_APPEARANCE_FILE" - exit 1 -fi - -# Cleanup -rm -f "$TEMP_FILE" - -echo "Font white labeling completed successfully!" diff --git a/frontend/scripts/white_label/i18n_white_label.sh b/frontend/scripts/white_label/i18n_white_label.sh deleted file mode 100644 index 60152d1630..0000000000 --- a/frontend/scripts/white_label/i18n_white_label.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --company-name Set the custom company name" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --company-name \"MyCompany Ltd.\"" -} - -CUSTOM_COMPANY_NAME="" -I18N_DIR="resources/translations" - -while [[ $# -gt 0 ]]; do - case $1 in - --company-name) - CUSTOM_COMPANY_NAME="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$CUSTOM_COMPANY_NAME" ]; then - echo "Error: Company name is required" - show_usage - exit 1 -fi - -if [ ! -d "$I18N_DIR" ]; then - echo "Error: Translation directory not found at $I18N_DIR" - exit 1 -fi - -echo "Replacing 'AppFlowy' with '$CUSTOM_COMPANY_NAME' in translation files..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -echo "Processing translation files..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Check if directory exists and has JSON files - if [ ! -d "$I18N_DIR" ] || [ -z "$(ls -A "$I18N_DIR"/*.json 2>/dev/null)" ]; then - echo "Error: No JSON files found in $I18N_DIR directory" - exit 1 - fi - - # Process each JSON file in the directory - for file in "$I18N_DIR"/*.json; do - echo "Updating $(basename "$file")" - # Use jq to replace AppFlowy with custom company name in values only - if command -v jq >/dev/null 2>&1; then - # Create a temporary file for the transformation - jq --arg company "$CUSTOM_COMPANY_NAME" 'walk(if type == "string" then gsub("AppFlowy"; $company) else . end)' "$file" > "${file}.tmp" - # Check if transformation was successful - if [ $? -eq 0 ]; then - mv "${file}.tmp" "$file" - else - echo "Error: Failed to process $file with jq" - rm -f "${file}.tmp" - exit 1 - fi - else - # Fallback to sed if jq is not available - # First, escape any special characters in the company name - ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') - # Replace AppFlowy with the custom company name in JSON values - sed $SED_INPLACE 's/\(".*"\): *"\(.*\)AppFlowy\(.*\)"/\1: "\2'"$ESCAPED_COMPANY_NAME"'\3"/g' "$file" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $file with sed" - exit 1 - fi - fi - done -else - for file in $(find "$I18N_DIR" -name "*.json" -type f); do - echo "Updating $(basename "$file")" - # Use jq to only replace values, not keys - if command -v jq >/dev/null 2>&1; then - jq 'walk(if type == "string" then gsub("AppFlowy"; "'"$CUSTOM_COMPANY_NAME"'") else . end)' "$file" > "$file.tmp" && mv "$file.tmp" "$file" - else - # Fallback to sed with a more specific pattern that targets values but not keys - sed $SED_INPLACE 's/: *"[^"]*AppFlowy[^"]*"/: "&"/g; s/: *"&"/: "'"$CUSTOM_COMPANY_NAME"'"/g' "$file" - # Fix any double colons that might have been introduced - sed $SED_INPLACE 's/: *: */: /g' "$file" - fi - done -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/icon_white_label.sh b/frontend/scripts/white_label/icon_white_label.sh deleted file mode 100644 index ca70bc1661..0000000000 --- a/frontend/scripts/white_label/icon_white_label.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --icon-path Set the path to the folder containing application icons (.svg files)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --icon-path \"/path/to/icons_folder\"" -} - -NEW_ICON_PATH="" -ICON_DIR="resources/flowy_icons" -ICON_NAME_NEED_REPLACE=("app_logo.svg" "ai_chat_logo.svg" "app_logo_with_text_light.svg" "app_logo_with_text_dark.svg") - -while [[ $# -gt 0 ]]; do - case $1 in - --icon-path) - NEW_ICON_PATH="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$NEW_ICON_PATH" ]; then - echo "Error: Icon path is required" - show_usage - exit 1 -fi - -if [ ! -d "$NEW_ICON_PATH" ]; then - echo "Error: New icon directory not found at $NEW_ICON_PATH" - exit 1 -fi - -if [ ! -d "$ICON_DIR" ]; then - echo "Error: Icon directory not found at $ICON_DIR" - exit 1 -fi - -echo "Replacing icons..." - -echo "Processing icon files..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - for subdir in "${ICON_DIR}"/*/; do - if [ -d "$subdir" ]; then - echo "Checking subdirectory: $(basename "$subdir")" - for file in "${subdir}"*.svg; do - if [ -f "$file" ] && [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - new_icon="${NEW_ICON_PATH}/$(basename "$file")" - if [ -f "$new_icon" ]; then - echo "Updating: $(basename "$subdir")/$(basename "$file")" - cp "$new_icon" "$file" - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") in $(basename "$subdir") with new icon" - else - echo "Error: Failed to replace $(basename "$file") in $(basename "$subdir")" - exit 1 - fi - else - echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" - fi - fi - done - fi - done -else - for file in $(find "$ICON_DIR" -name "*.svg" -type f); do - if [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - new_icon="${NEW_ICON_PATH}/$(basename "$file")" - if [ -f "$new_icon" ]; then - echo "Updating: $(basename "$file")" - cp "$new_icon" "$file" - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") with new icon" - else - echo "Error: Failed to replace $(basename "$file")" - exit 1 - fi - else - echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" - fi - fi - done -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/resources/my_company_logo.ico b/frontend/scripts/white_label/resources/my_company_logo.ico deleted file mode 100644 index c922a6b36d..0000000000 Binary files a/frontend/scripts/white_label/resources/my_company_logo.ico and /dev/null differ diff --git a/frontend/scripts/white_label/resources/my_company_logo.png b/frontend/scripts/white_label/resources/my_company_logo.png deleted file mode 100644 index 8f50872743..0000000000 Binary files a/frontend/scripts/white_label/resources/my_company_logo.png and /dev/null differ diff --git a/frontend/scripts/white_label/resources/my_company_logo.svg b/frontend/scripts/white_label/resources/my_company_logo.svg deleted file mode 100644 index c06bf17cb4..0000000000 --- a/frontend/scripts/white_label/resources/my_company_logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/scripts/white_label/white_label.sh b/frontend/scripts/white_label/white_label.sh deleted file mode 100644 index 8ecd187210..0000000000 --- a/frontend/scripts/white_label/white_label.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/bash - -# Default values -APP_NAME="AppFlowy" -APP_IDENTIFIER="com.appflowy.appflowy" -COMPANY_NAME="AppFlowy Inc." -COPYRIGHT="Copyright © 2025 AppFlowy Inc." -ICON_PATH="" -WINDOWS_ICON_PATH="" -FONT_PATH="" -FONT_FAMILY="" -PLATFORMS=("windows" "linux" "macos" "ios" "android") - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --app-name Set the application name" - echo " --app-identifier Set the application identifier" - echo " --company-name Set the company name" - echo " --copyright Set the copyright information" - echo " --icon-path Set the path to the application icon (.svg)" - echo " --windows-icon-path Set the path to the windows application icon (.ico)" - echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" - echo " --font-family Set the name of the font family" - echo " --platforms Comma-separated list of platforms to white label (windows,linux,macos,ios,android)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" - echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" - echo " --platforms \"windows,linux,macos\" \\" - echo " --windows-icon-path \"./assets/icons/mycompany.ico\" \\" - echo " --icon-path \"./assets/icons/\" \\" - echo " --font-path \"./assets/fonts/\" --font-family \"CustomFont\"" -} - -while [[ $# -gt 0 ]]; do - case $1 in - --app-name) - APP_NAME="$2" - shift 2 - ;; - --app-identifier) - APP_IDENTIFIER="$2" - shift 2 - ;; - --company-name) - COMPANY_NAME="$2" - shift 2 - ;; - --copyright) - COPYRIGHT="$2" - shift 2 - ;; - --icon-path) - ICON_PATH="$2" - shift 2 - ;; - --windows-icon-path) - WINDOWS_ICON_PATH="$2" - shift 2 - ;; - --font-path) - FONT_PATH="$2" - shift 2 - ;; - --font-family) - FONT_FAMILY="$2" - shift 2 - ;; - --platforms) - IFS=',' read -ra PLATFORMS <<< "$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$APP_NAME" ] || [ -z "$APP_IDENTIFIER" ] || [ -z "$COMPANY_NAME" ] || [ -z "$COPYRIGHT" ] || [ -z "$ICON_PATH" ]; then - echo "Error: All parameters are required" - show_usage - exit 1 -fi - -if [ ! -d "$ICON_PATH" ]; then - echo "Error: Icon directory not found at $ICON_PATH" - exit 1 -fi - -if [ ! -f "$WINDOWS_ICON_PATH" ]; then - echo "Error: Windows icon file not found at $WINDOWS_ICON_PATH" - exit 1 -fi - -run_platform_script() { - local platform=$1 - local script_path="scripts/white_label/${platform}_white_label.sh" - - if [ ! -f "$script_path" ]; then - echo -e "\033[31mWarning: White label script not found for platform: $platform\033[0m" - return - fi - - echo -e "\033[32mRunning white label script for $platform...\033[0m" - bash "$script_path" \ - --app-name "$APP_NAME" \ - --app-identifier "$APP_IDENTIFIER" \ - --company-name "$COMPANY_NAME" \ - --copyright "$COPYRIGHT" \ - --icon-path "$WINDOWS_ICON_PATH" -} - -echo -e "\033[32mRunning i18n white label script...\033[0m" -bash "scripts/white_label/i18n_white_label.sh" --company-name "$COMPANY_NAME" - -echo -e "\033[32mRunning icon white label script...\033[0m" -bash "scripts/white_label/icon_white_label.sh" --icon-path "$ICON_PATH" - -echo -e "\033[32mRunning code white label script...\033[0m" -bash "scripts/white_label/code_white_label.sh" --company-name "$COMPANY_NAME" - -# Run font white label script if font parameters are provided -if [ ! -z "$FONT_PATH" ] && [ ! -z "$FONT_FAMILY" ]; then - echo -e "\033[32mRunning font white label script...\033[0m" - bash "scripts/white_label/font_white_label.sh" \ - --font-path "$FONT_PATH" \ - --font-family "$FONT_FAMILY" -fi - -for platform in "${PLATFORMS[@]}"; do - run_platform_script "$platform" -done - -echo -e "\033[32mWhite labeling process completed successfully!\033[0m" diff --git a/frontend/scripts/white_label/windows_white_label.sh b/frontend/scripts/white_label/windows_white_label.sh deleted file mode 100644 index 58801424ff..0000000000 --- a/frontend/scripts/white_label/windows_white_label.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -APP_NAME="AppFlowy" -APP_IDENTIFIER="com.appflowy.appflowy" -COMPANY_NAME="AppFlowy Inc." -COPYRIGHT="Copyright © 2025 AppFlowy Inc." -ICON_PATH="" - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --app-name Set the application name" - echo " --app-identifier Set the application identifier" - echo " --company-name Set the company name" - echo " --copyright Set the copyright information" - echo " --icon-path Set the path to the application icon (.ico file)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" - echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" - echo " --icon-path \"./assets/icons/company.ico\"" -} - -while [[ $# -gt 0 ]]; do - case $1 in - --app-name) - APP_NAME="$2" - shift 2 - ;; - --app-identifier) - APP_IDENTIFIER="$2" - shift 2 - ;; - --company-name) - COMPANY_NAME="$2" - shift 2 - ;; - --copyright) - COPYRIGHT="$2" - shift 2 - ;; - --icon-path) - ICON_PATH="$2" - shift 2 - ;; - --output-dir) - OUTPUT_DIR="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$APP_NAME" ]; then - echo -e "\033[31mError: Application name is required\033[0m" - exit 1 -fi - -if [ -z "$APP_IDENTIFIER" ]; then - echo -e "\033[31mError: Application identifier is required\033[0m" - exit 1 -fi - -if [ -z "$COMPANY_NAME" ]; then - echo -e "\033[31mError: Company name is required\033[0m" - exit 1 -fi - -if [ -z "$COPYRIGHT" ]; then - echo -e "\033[31mError: Copyright information is required\033[0m" - exit 1 -fi - -if [ -z "$ICON_PATH" ]; then - echo -e "\033[31mError: Icon path is required\033[0m" - exit 1 -fi - -echo "Starting Windows application customization..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -update_runner_files() { - runner_dir="appflowy_flutter/windows/runner" - - if [ -f "$runner_dir/Runner.rc" ]; then - sed $SED_INPLACE "s/VALUE \"CompanyName\", .*$/VALUE \"CompanyName\", \"$COMPANY_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"FileDescription\", .*$/VALUE \"FileDescription\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"InternalName\", .*$/VALUE \"InternalName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"OriginalFilename\", .*$/VALUE \"OriginalFilename\", \"$APP_NAME.exe\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"LegalCopyright\", .*$/VALUE \"LegalCopyright\", \"$COPYRIGHT\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"ProductName\", .*$/VALUE \"ProductName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - echo -e "Runner.rc updated successfully" - else - echo -e "\033[31mRunner.rc file not found\033[0m" - fi -} - -update_icon() { - if [ ! -z "$ICON_PATH" ] && [ -f "$ICON_PATH" ]; then - app_icon_path="appflowy_flutter/windows/runner/resources/app_icon.ico" - cp "$ICON_PATH" "$app_icon_path" - echo -e "Application icon updated successfully" - else - echo -e "\033[31mApplication icon file not found\033[0m" - fi -} - -update_cmake_lists() { - cmake_file="appflowy_flutter/windows/CMakeLists.txt" - if [ -f "$cmake_file" ]; then - sed $SED_INPLACE "s/set(BINARY_NAME .*)$/set(BINARY_NAME \"$APP_NAME\")/" "$cmake_file" - echo -e "CMake configuration updated successfully" - else - echo -e "\033[31mCMake configuration file not found\033[0m" - fi -} - -update_main_cpp() { - main_cpp_file="appflowy_flutter/windows/runner/main.cpp" - if [ -f "$main_cpp_file" ]; then - sed $SED_INPLACE "s/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"AppFlowyMutex\");/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"${APP_NAME}Mutex\");/" "$main_cpp_file" - sed $SED_INPLACE "s/HWND handle = FindWindowA(NULL, \"AppFlowy\");/HWND handle = FindWindowA(NULL, \"$APP_NAME\");/" "$main_cpp_file" - sed $SED_INPLACE "s/if (window.SendAppLinkToInstance(L\"AppFlowy\")) {/if (window.SendAppLinkToInstance(L\"$APP_NAME\")) {/" "$main_cpp_file" - sed $SED_INPLACE "s/if (!window.Create(L\"AppFlowy\", origin, size)) {/if (!window.Create(L\"$APP_NAME\", origin, size)) {/" "$main_cpp_file" - echo -e "main.cpp updated successfully" - else - echo -e "\033[31mMain.cpp file not found\033[0m" - fi -} - -echo "Applying customizations..." -update_runner_files -update_icon -update_cmake_lists -update_main_cpp - -echo "Windows application customization completed successfully!"