chore: Merge branch 'main' into develop

This commit is contained in:
nathan 2023-05-04 12:39:46 +08:00
commit ad99998d33
146 changed files with 2576 additions and 1572 deletions

View File

@ -8,6 +8,7 @@ on:
- "release/*" - "release/*"
paths: paths:
- "frontend/**" - "frontend/**"
- "!frontend/appflowy_tauri/**"
pull_request: pull_request:
branches: branches:
@ -16,6 +17,7 @@ on:
- "release/*" - "release/*"
paths: paths:
- "frontend/**" - "frontend/**"
- "!frontend/appflowy_tauri/**"
env: env:
FLUTTER_VERSION: "3.7.5" FLUTTER_VERSION: "3.7.5"

View File

@ -236,14 +236,6 @@ jobs:
extra-build-args: "", extra-build-args: "",
flutter_profile: production-linux-x86_64, flutter_profile: production-linux-x86_64,
} }
# - { arch: aarch64, target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-aarch64 }
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-18.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@ -13,4 +13,6 @@ jobs:
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with: with:
args: '@appflowytranslators English UI strings has been updated.' args: |
@appflowytranslators English UI strings has been updated.
Link to changes: ${{github.event.compare}}

View File

@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi" LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.1.2" CURRENT_APP_VERSION = "0.1.3"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy" PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

View File

@ -0,0 +1,422 @@
{
"appName": "AppFlowy",
"defaultUsername": "أنا",
"welcomeText": "مرحبًا بك في @: appName",
"githubStarText": "نجمة على GitHub",
"subscribeNewsletterText": "اشترك في النشرة الإخبارية",
"letsGoButtonText": "بداية سريعة",
"title": "عنوان",
"signUp": {
"buttonText": "اشتراك",
"title": "قم بالتسجيل في @: appName",
"getStartedText": "البدء",
"emptyPasswordError": "لا يمكن أن تكون كلمة المرور فارغة",
"repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة",
"unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور",
"alreadyHaveAnAccount": "هل لديك حساب؟",
"emailHint": "بريد إلكتروني",
"passwordHint": "كلمة المرور",
"repeatPasswordHint": "اعد كلمة السر"
},
"signIn": {
"loginTitle": "تسجيل الدخول إلى @: appName",
"loginButtonText": "تسجيل الدخول",
"buttonText": "تسجيل الدخول",
"forgotPassword": "هل نسيت كلمة السر؟",
"emailHint": "بريد إلكتروني",
"passwordHint": "كلمة المرور",
"dontHaveAnAccount": "ليس لديك حساب؟",
"repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة",
"unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور"
},
"workspace": {
"create": "قم بإنشاء مساحة عمل",
"hint": "مساحة العمل",
"notFoundError": "مساحة العمل غير موجودة"
},
"shareAction": {
"buttonText": "مشاركه",
"workInProgress": "قريباً",
"markdown": "Markdown",
"copyLink": "نسخ الرابط"
},
"moreAction": {
"small": "صغير",
"medium": "متوسط",
"large": "كبير",
"fontSize": "حجم الخط",
"import": "استيراد"
},
"disclosureAction": {
"rename": "إعادة تسمية",
"delete": "يمسح",
"duplicate": "كرر"
},
"blankPageTitle": "صفحة فارغة",
"newPageText": "صفحة جديدة",
"trash": {
"text": "المهملات",
"restoreAll": "استعادة الكل",
"deleteAll": "حذف الكل",
"pageHeader": {
"fileName": "اسم الملف",
"lastModified": "آخر تعديل",
"created": "تم انشاؤها"
}
},
"deletePagePrompt": {
"text": "هذه الصفحة في المهملات",
"restore": "استعادة الصفحة",
"deletePermanent": "الحذف بشكل نهائي"
},
"dialogCreatePageNameHint": "اسم الصفحة",
"questionBubble": {
"shortcuts": "الاختصارات",
"whatsNew": "ما هو الجديد؟",
"help": "المساعدة والدعم",
"markdown": "Markdown",
"debug": {
"name": "معلومات التصحيح",
"success": "تم نسخ معلومات التصحيح إلى الحافظة!",
"fail": "تعذر نسخ معلومات التصحيح إلى الحافظة"
}
},
"menuAppHeader": {
"addPageTooltip": "أضف صفحة في الداخل بسرعة",
"defaultNewPageName": "بدون عنوان",
"renameDialog": "إعادة تسمية"
},
"toolbar": {
"undo": "الغاء التحميل",
"redo": "إعادة",
"bold": "عريض",
"italic": "مائل",
"underline": "تسطير",
"strike": "يتوسطه خط",
"numList": "قائمة مرقمة",
"bulletList": "قائمة نقطية",
"checkList": "قائمة تدقيق",
"inlineCode": "رمز مضمّن",
"quote": "كتلة اقتباس",
"header": "رأس",
"highlight": "تسليط الضوء",
"color": "لون"
},
"tooltip": {
"lightMode": "قم بالتبديل إلى وضع الإضاءة",
"darkMode": "قم بالتبديل إلى الوضع الداكن",
"openAsPage": "فتح كصفحة",
"addNewRow": "أضف صفًا جديدًا",
"openMenu": "انقر لفتح القائمة",
"viewDataBase": "عرض قاعدة البيانات",
"referencePage": "تمت الإشارة إلى هذا {name}"
},
"sideBar": {
"closeSidebar": "إغلاق الشريط الجانبي",
"openSidebar": "فتح الشريط الجانبي"
},
"notifications": {
"export": {
"markdown": "تم تصدير ملاحظة إلى Markdown",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "جهات الاتصال",
"whatsHappening": "ماذا يحدث هذا الاسبوع؟",
"addContact": "إضافة جهة اتصال",
"editContact": "تحرير جهة الاتصال"
},
"button": {
"OK": "نعم",
"Done": "منتهي",
"Cancel": "إلغاء",
"signIn": "تسجيل الدخول",
"signOut": "خروج",
"complete": "مكتمل",
"save": "حفظ",
"generate": "يولد",
"esc": "خروج",
"keep": "ابقاء",
"tryAgain": "حاول ثانية",
"discard": "تجاهل",
"replace": "يستبدل",
"insertBelow": "إدراج أدناه"
},
"label": {
"welcome": "مرحباً!",
"firstName": "الاسم الأول",
"middleName": "الاسم الأوسط",
"lastName": "اسم العائلة",
"stepX": "الخطوة {X}"
},
"oAuth": {
"err": {
"failedTitle": "غير قادر على الاتصال بحسابك.",
"failedMsg": "يرجى التأكد من إكمال عملية تسجيل الدخول في متصفحك."
},
"google": {
"title": "تسجيل الدخول إلى GOOGLE",
"instruction1": "لاستيراد جهات اتصال Google الخاصة بك ، ستحتاج إلى ترخيص هذا التطبيق باستخدام متصفح الويب الخاص بك.",
"instruction2": "انسخ هذا الرمز إلى الحافظة الخاصة بك عن طريق النقر فوق الرمز أو تحديد النص:",
"instruction3": "انتقل إلى الرابط التالي في متصفح الويب الخاص بك ، وأدخل الرمز أعلاه:",
"instruction4": "اضغط على الزر أدناه عند الانتهاء من التسجيل:"
}
},
"settings": {
"title": "إعدادات",
"menu": {
"appearance": "مظهر",
"language": "لغة",
"user": "مستخدم",
"files": "الملفات",
"open": "أفتح الإعدادات"
},
"appearance": {
"themeMode": {
"label": "وضع السمة",
"light": "وضع الضوء",
"dark": "الوضع الداكن",
"system": "التكيف مع النظام"
},
"theme": "سمة"
},
"files": {
"defaultLocation": "أين يتم تخزين بياناتك الآن",
"doubleTapToCopy": "انقر نقرًا مزدوجًا لنسخ المسار",
"restoreLocation": "استعادة المسار الافتراضي AppFlowy",
"customizeLocation": "افتح مجلدًا آخر",
"restartApp": "يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.",
"exportDatabase": "تصدير قاعدة البيانات",
"selectFiles": "حدد الملفات التي تريد تصديرها",
"createNewFolder": "انشاء مجلد جديد",
"createNewFolderDesc": "أخبرنا بالمكان الذي تريد تخزين بياناتك فيه",
"open": "يفتح",
"openFolder": "افتح مجلدًا موجودًا",
"openFolderDesc": "اقرأها واكتبها في مجلد AppFlowy الموجود لديك",
"folderHintText": "إسم الملف",
"location": "إنشاء مجلد جديد",
"locationDesc": "اختر اسمًا لمجلد بيانات AppFlowy",
"browser": "تصفح",
"create": "يخلق",
"folderPath": "مسار لتخزين المجلد الخاص بك",
"locationCannotBeEmpty": "لا يمكن أن يكون المسار فارغًا",
"pathCopiedSnackbar": "تم نسخ مسار تخزين الملفات إلى الحافظة!"
},
"user": {
"name": "اسم",
"icon": "أيقونة",
"selectAnIcon": "حدد أيقونة",
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
}
},
"grid": {
"settings": {
"filter": "منقي",
"sort": "نوع",
"sortBy": "ترتيب حسب",
"Properties": "ملكيات",
"group": "مجموعة",
"addFilter": "أضف عامل تصفية",
"deleteFilter": "حذف عامل التصفية",
"filterBy": "مصنف بواسطة...",
"typeAValue": "اكتب قيمة ...",
"layout": "تَخطِيط"
},
"textFilter": {
"contains": "يتضمن",
"doesNotContain": "لا يحتوي",
"endsWith": "ينتهي بـ",
"startWith": "ابدا ب",
"is": "يكون",
"isNot": "ليس",
"isEmpty": "فارغ",
"isNotEmpty": "ليس فارغا",
"choicechipPrefix": {
"isNot": "لا",
"startWith": "ابدا ب",
"endWith": "ينتهي بـ",
"isEmpty": "فارغ",
"isNotEmpty": "ليس فارغا"
}
},
"checkboxFilter": {
"isChecked": "التحقق",
"isUnchecked": "لم يتم التحقق منه",
"choicechipPrefix": {
"is": "يكون"
}
},
"checklistFilter": {
"isComplete": "كاملة",
"isIncomplted": "غير مكتمل"
},
"singleSelectOptionFilter": {
"is": "يكون",
"isNot": "ليس",
"isEmpty": "فارغ",
"isNotEmpty": "ليس فارغا"
},
"multiSelectOptionFilter": {
"contains": "يتضمن",
"doesNotContain": "لا يحتوي",
"isEmpty": "فارغ",
"isNotEmpty": "ليس فارغا"
},
"field": {
"hide": "يخفي",
"insertLeft": "أدخل اليسار",
"insertRight": "أدخل اليمين",
"duplicate": "ينسخ",
"delete": "يمسح",
"textFieldName": "نص",
"checkboxFieldName": "خانة اختيار",
"dateFieldName": "تاريخ",
"numberFieldName": "أعداد",
"singleSelectFieldName": "يختار",
"multiSelectFieldName": "تحديد متعدد",
"urlFieldName": "URL",
"checklistFieldName": "قائمة تدقيق",
"numberFormat": "تنسيق الأرقام",
"dateFormat": "صيغة التاريخ",
"includeTime": "أضف الوقت",
"dateFormatFriendly": "شهر يوم سنه",
"dateFormatISO": "سنة شهر يوم",
"dateFormatLocal": "شهر يوم سنه",
"dateFormatUS": "سنة شهر يوم",
"dateFormatDayMonthYear": "يوم شهر سنة",
"timeFormat": "تنسيق الوقت",
"invalidTimeFormat": "تنسيق غير صالح",
"timeFormatTwelveHour": "12 ساعة",
"timeFormatTwentyFourHour": "24 ساعة",
"addSelectOption": "أضف خيارًا",
"optionTitle": "خيارات",
"addOption": "إضافة خيار",
"editProperty": "تحرير الملكية",
"newProperty": "خاصية جديدة",
"deleteFieldPromptMessage": "هل أنت متأكد؟ سيتم حذف هذه الخاصية"
},
"sort": {
"ascending": "تصاعدي",
"descending": "تنازلي",
"deleteSort": "حذف الفرز",
"addSort": "أضف نوعًا"
},
"row": {
"duplicate": "مكرره",
"delete": "يمسح",
"textPlaceholder": "فارغ",
"copyProperty": "نسخ الممتلكات إلى الحافظة",
"count": "عدد",
"newRow": "صف جديد"
},
"selectOption": {
"create": "يخلق",
"purpleColor": "أرجواني",
"pinkColor": "لون القرنفل",
"lightPinkColor": "وردي فاتح",
"orangeColor": "البرتقالي",
"yellowColor": "أصفر",
"limeColor": "جير",
"greenColor": "أخضر",
"aquaColor": "أكوا",
"blueColor": "أزرق",
"deleteTag": "حذف العلامة",
"colorPanelTitle": "الألوان",
"panelTitle": "حدد خيارًا أو أنشئ خيارًا",
"searchOption": "ابحث عن خيار"
},
"checklist": {
"panelTitle": "أضف عنصرًا"
},
"menuName": "شبكة",
"referencedGridPrefix": "نظرا ل"
},
"document": {
"menuName": "وثيقة",
"date": {
"timeHintTextInTwelveHour": "01:00 مساءً",
"timeHintTextInTwentyFourHour": "13:00"
},
"slashMenu": {
"board": {
"selectABoardToLinkTo": "حدد لوحة للارتباط بها",
"createANewBoard": "قم بإنشاء لوحة جديدة"
},
"grid": {
"selectAGridToLinkTo": "حدد الشبكة للارتباط بها",
"createANewGrid": "قم بإنشاء شبكة جديدة"
}
},
"plugins": {
"referencedBoard": "المجلس المشار إليه",
"referencedGrid": "الشبكة المشار إليها",
"autoGeneratorMenuItemName": "كاتب OpenAI",
"autoGeneratorTitleName": "OpenAI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...",
"autoGeneratorLearnMore": "يتعلم أكثر",
"autoGeneratorGenerate": "يولد",
"autoGeneratorHintText": "اسأل OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح OpenAI",
"smartEdit": "مساعدي الذكاء الاصطناعي",
"openAI": "OpenAI",
"smartEditFixSpelling": "أصلح التهجئة",
"warning": "⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.",
"smartEditSummarize": "لخص",
"smartEditCouldNotFetchResult": "تعذر جلب النتيجة من OpenAI",
"smartEditCouldNotFetchKey": "تعذر جلب مفتاح OpenAI",
"smartEditDisabled": "قم بتوصيل OpenAI في الإعدادات",
"discardResponse": "هل تريد تجاهل استجابات الذكاء الاصطناعي؟",
"cover": {
"changeCover": "تبديل الغطاء",
"colors": "الألوان",
"images": "الصور",
"clearAll": "امسح الكل",
"abstract": "خلاصة",
"addCover": "أضف الغلاف",
"addLocalImage": "أضف الصورة المحلية",
"invalidImageUrl": "عنوان URL للصورة غير صالح",
"failedToAddImageToGallery": "فشل في إضافة الصورة إلى المعرض",
"enterImageUrl": "أدخل عنوان URL للصورة",
"add": "يضيف",
"back": "خلف",
"saveToGallery": "حفظ في المعرض",
"removeIcon": "إزالة الرمز",
"pasteImageUrl": "لصق عنوان URL للصورة",
"or": "أو",
"pickFromFiles": "اختر من الملفات",
"couldNotFetchImage": "تعذر جلب الصورة",
"imageSavingFailed": "فشل حفظ الصورة",
"addIcon": "إضافة أيقونة",
"coverRemoveAlert": "ستتم إزالته من الغلاف بعد حذفه.",
"alertDialogConfirmation": "هل أنت متأكد أنك تريد الاستمرار؟"
},
"mathEquation": {
"addMathEquation": "أضف معادلة رياضية",
"editMathEquation": "تحرير المعادلة الرياضية"
}
}
},
"board": {
"column": {
"create_new_card": "جديد"
},
"menuName": "سبورة",
"referencedBoardPrefix": "نظرا ل"
},
"calendar": {
"menuName": "تقويم",
"defaultNewCalendarTitle": "بدون عنوان",
"navigation": {
"today": "اليوم",
"jumpToday": "انتقل إلى اليوم",
"previousMonth": "الشهر الماضى",
"nextMonth": "الشهر القادم"
},
"settings": {
"showWeekNumbers": "إظهار أرقام الأسبوع",
"showWeekends": "عرض عطلات نهاية الأسبوع",
"firstDayOfWeek": "اليوم الأول من الأسبوع",
"layoutDateField": "تقويم التخطيط بواسطة"
}
}
}

View File

@ -74,6 +74,7 @@
"shortcuts": "Shortcuts", "shortcuts": "Shortcuts",
"whatsNew": "What's new?", "whatsNew": "What's new?",
"help": "Help & Support", "help": "Help & Support",
"markdown": "Markdown",
"debug": { "debug": {
"name": "Debug Info", "name": "Debug Info",
"success": "Copied debug info to clipboard!", "success": "Copied debug info to clipboard!",
@ -128,6 +129,7 @@
}, },
"button": { "button": {
"OK": "OK", "OK": "OK",
"Done": "Done",
"Cancel": "Cancel", "Cancel": "Cancel",
"signIn": "Sign In", "signIn": "Sign In",
"signOut": "Sign Out", "signOut": "Sign Out",
@ -278,7 +280,7 @@
"numberFormat": "Number format", "numberFormat": "Number format",
"dateFormat": "Date format", "dateFormat": "Date format",
"includeTime": "Include time", "includeTime": "Include time",
"dateFormatFriendly": "Month Day,Year", "dateFormatFriendly": "Month Day, Year",
"dateFormatISO": "Year-Month-Day", "dateFormatISO": "Year-Month-Day",
"dateFormatLocal": "Month/Day/Year", "dateFormatLocal": "Month/Day/Year",
"dateFormatUS": "Year/Month/Day", "dateFormatUS": "Year/Month/Day",
@ -306,7 +308,8 @@
"textPlaceholder": "Empty", "textPlaceholder": "Empty",
"copyProperty": "Copied property to clipboard", "copyProperty": "Copied property to clipboard",
"count": "Count", "count": "Count",
"newRow": "New row" "newRow": "New row",
"action": "Action"
}, },
"selectOption": { "selectOption": {
"create": "Create", "create": "Create",
@ -360,6 +363,7 @@
"smartEditFixSpelling": "Fix spelling", "smartEditFixSpelling": "Fix spelling",
"warning": "⚠️ AI responses can be inaccurate or misleading.", "warning": "⚠️ AI responses can be inaccurate or misleading.",
"smartEditSummarize": "Summarize", "smartEditSummarize": "Summarize",
"smartEditImproveWriting": "Improve Writing",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key", "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings", "smartEditDisabled": "Connect OpenAI in Settings",
@ -387,6 +391,10 @@
"addIcon": "Add Icon", "addIcon": "Add Icon",
"coverRemoveAlert": "It will be removed from cover after it is deleted.", "coverRemoveAlert": "It will be removed from cover after it is deleted.",
"alertDialogConfirmation": "Are you sure, you want to continue?" "alertDialogConfirmation": "Are you sure, you want to continue?"
},
"mathEquation": {
"addMathEquation": "Add Math Equation",
"editMathEquation": "Edit Math Equation"
} }
} }
}, },
@ -409,7 +417,7 @@
"settings": { "settings": {
"showWeekNumbers": "Show week numbers", "showWeekNumbers": "Show week numbers",
"showWeekends": "Show weekends", "showWeekends": "Show weekends",
"firstDayOfWeek": "First day of week", "firstDayOfWeek": "Start week on",
"layoutDateField": "Layout calendar by" "layoutDateField": "Layout calendar by"
} }
} }

View File

@ -175,7 +175,7 @@
"numberFormat": "Format angka", "numberFormat": "Format angka",
"dateFormat": "Format tanggal", "dateFormat": "Format tanggal",
"includeTime": "Sertakan waktu", "includeTime": "Sertakan waktu",
"dateFormatFriendly": "Bulan Hari,Tahun", "dateFormatFriendly": "Bulan Hari, Tahun",
"dateFormatISO": "Tahun-Bulan-Hari", "dateFormatISO": "Tahun-Bulan-Hari",
"dateFormatLocal": "Bulan/Hari/Tahun", "dateFormatLocal": "Bulan/Hari/Tahun",
"dateFormatUS": "Tahun/Bulan/Hari", "dateFormatUS": "Tahun/Bulan/Hari",

View File

@ -167,7 +167,7 @@
"numberFormat": "数値書式", "numberFormat": "数値書式",
"dateFormat": "日付書式", "dateFormat": "日付書式",
"includeTime": "時刻を含める", "includeTime": "時刻を含める",
"dateFormatFriendly": "月 日,年", "dateFormatFriendly": "月 日, 年",
"dateFormatISO": "年-月-日", "dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年", "dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日", "dateFormatUS": "年/月/日",

View File

@ -179,7 +179,7 @@
"numberFormat": "숫자 형식", "numberFormat": "숫자 형식",
"dateFormat": "날짜 형식", "dateFormat": "날짜 형식",
"includeTime": "시간 표시", "includeTime": "시간 표시",
"dateFormatFriendly": "월 일,년", "dateFormatFriendly": "월 일, 년",
"dateFormatISO": "년-월-일", "dateFormatISO": "년-월-일",
"dateFormatLocal": "월/일/년", "dateFormatLocal": "월/일/년",
"dateFormatUS": "년/월/일", "dateFormatUS": "년/월/일",

View File

@ -275,7 +275,7 @@
"numberFormat": "Formato numérico", "numberFormat": "Formato numérico",
"dateFormat": "Formato de data", "dateFormat": "Formato de data",
"includeTime": "Incluir hora", "includeTime": "Incluir hora",
"dateFormatFriendly": "Mês Dia,Ano", "dateFormatFriendly": "Mês Dia, Ano",
"dateFormatISO": "Ano-Mês-Dia", "dateFormatISO": "Ano-Mês-Dia",
"dateFormatLocal": "Mês/Dia/Ano", "dateFormatLocal": "Mês/Dia/Ano",
"dateFormatUS": "Ano/Mês/Dia", "dateFormatUS": "Ano/Mês/Dia",

View File

@ -44,7 +44,8 @@
"small": "маленький", "small": "маленький",
"medium": "средний", "medium": "средний",
"large": "большой", "large": "большой",
"fontSize": "Размер шрифта" "fontSize": "Размер шрифта",
"import": "Импортировать"
}, },
"disclosureAction": { "disclosureAction": {
"rename": "Переименовать", "rename": "Переименовать",
@ -70,8 +71,10 @@
}, },
"dialogCreatePageNameHint": "Имя страницы", "dialogCreatePageNameHint": "Имя страницы",
"questionBubble": { "questionBubble": {
"shortcuts": "Комбинации клавиш",
"whatsNew": "Что нового?", "whatsNew": "Что нового?",
"help": "Помощь", "help": "Помощь",
"markdown": "Markdown",
"debug": { "debug": {
"name": "Отладочная информация", "name": "Отладочная информация",
"success": "Скопировано в буфер обмена!", "success": "Скопировано в буфер обмена!",
@ -126,6 +129,7 @@
}, },
"button": { "button": {
"OK": "OK", "OK": "OK",
"Done": "Завершить",
"Cancel": "Отмена", "Cancel": "Отмена",
"signIn": "Войти", "signIn": "Войти",
"signOut": "Выйти", "signOut": "Выйти",
@ -170,7 +174,7 @@
}, },
"appearance": { "appearance": {
"themeMode": { "themeMode": {
"label": "Режим темы", "label": "Тема приложения",
"light": "Светлая", "light": "Светлая",
"dark": "Тёмная", "dark": "Тёмная",
"system": "Системная" "system": "Системная"
@ -197,7 +201,7 @@
"create": "Создать", "create": "Создать",
"folderPath": "Путь к вашей папке", "folderPath": "Путь к вашей папке",
"locationCannotBeEmpty": "Путь не может быть пустым", "locationCannotBeEmpty": "Путь не может быть пустым",
"pathCopiedSnackbar": "File storage path copied to clipboard!" "pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
}, },
"user": { "user": {
"name": "Имя", "name": "Имя",
@ -216,7 +220,8 @@
"addFilter": "Добавить фильтр", "addFilter": "Добавить фильтр",
"deleteFilter": "Удалить фильтр", "deleteFilter": "Удалить фильтр",
"filterBy": "Фильтровать по...", "filterBy": "Фильтровать по...",
"typeAValue": "Введите значение..." "typeAValue": "Введите значение...",
"layout": "Раскладка"
}, },
"textFilter": { "textFilter": {
"contains": "Содержит", "contains": "Содержит",
@ -303,7 +308,8 @@
"textPlaceholder": "Пусто", "textPlaceholder": "Пусто",
"copyProperty": "Свойство скопировано", "copyProperty": "Свойство скопировано",
"count": "Количество", "count": "Количество",
"newRow": "Новая строка" "newRow": "Новая строка",
"action": "Действия"
}, },
"selectOption": { "selectOption": {
"create": "Создать", "create": "Создать",
@ -384,6 +390,10 @@
"addIcon": "Добавить иконку", "addIcon": "Добавить иконку",
"coverRemoveAlert": "Изображение будет удалено с обложки", "coverRemoveAlert": "Изображение будет удалено с обложки",
"alertDialogConfirmation": "Вы хотите продолжить?" "alertDialogConfirmation": "Вы хотите продолжить?"
},
"mathEquation": {
"addMathEquation": "Добавить математическое выражение",
"editMathEquation": "Редактировать математическое выражение"
} }
} }
}, },

View File

@ -183,7 +183,7 @@
"numberFormat": "Sifferformat", "numberFormat": "Sifferformat",
"dateFormat": "Datumformat", "dateFormat": "Datumformat",
"includeTime": "Inkludera tid", "includeTime": "Inkludera tid",
"dateFormatFriendly": "Månad Dag,År", "dateFormatFriendly": "Månad Dag, År",
"dateFormatISO": "År-Månad-Dag", "dateFormatISO": "År-Månad-Dag",
"dateFormatLocal": "Månad/Dag/År", "dateFormatLocal": "Månad/Dag/År",
"dateFormatUS": "År/Månad/Dag", "dateFormatUS": "År/Månad/Dag",

View File

@ -183,7 +183,7 @@
"numberFormat": "数字格式", "numberFormat": "数字格式",
"dateFormat": "日期格式", "dateFormat": "日期格式",
"includeTime": "包含时间", "includeTime": "包含时间",
"dateFormatFriendly": "月 日,年", "dateFormatFriendly": "月 日, 年",
"dateFormatISO": "年-月-日", "dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年", "dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日", "dateFormatUS": "年/月/日",

View File

@ -278,7 +278,7 @@
"numberFormat": "數字格式", "numberFormat": "數字格式",
"dateFormat": "日期格式", "dateFormat": "日期格式",
"includeTime": "包含時間", "includeTime": "包含時間",
"dateFormatFriendly": "月 日,年", "dateFormatFriendly": "月 日, 年",
"dateFormatISO": "年-月-日", "dateFormatISO": "年-月-日",
"dateFormatLocal": "月/日/年", "dateFormatLocal": "月/日/年",
"dateFormatUS": "年/月/日", "dateFormatUS": "年/月/日",

View File

@ -0,0 +1,54 @@
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
/// Integration tests for an empty board. The [TestWorkspaceService] will load
/// a workspace from an empty board `assets/test/workspaces/board.zip` for all
/// tests.
///
/// To create another integration test with a preconfigured workspace.
/// Use the following steps.
/// 1. Create a new workspace from the AppFlowy launch screen.
/// 2. Modify the workspace until it is suitable as the starting point for
/// the integration test you need to land.
/// 3. Use a zip utility program to zip the workspace folder that you created.
/// 4. Add the zip file under `assets/test/workspaces/`
/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.
/// For example, if you added a workspace called `empty_calendar.zip`,
/// then [TestWorkspace] should have the following value:
/// ```dart
/// enum TestWorkspace {
/// board('board'),
/// empty_calendar('empty_calendar');
///
/// /* code */
/// }
/// ```
/// 6. Double check that the .zip file that you added is included as an asset in
/// the pubspec.yaml file under appflowy_flutter.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.coverImage);
group('cover image', () {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());
testWidgets(
'hovering on cover image will display change and delete cover image buttons',
(tester) async {
await tester.initializeAppFlowy();
expect(find.byType(Image), findsOneWidget);
final TestPointer pointer = TestPointer(1, PointerDeviceKind.mouse);
final imageFinder = find.byType(Image);
Offset offset = tester.getCenter(imageFinder);
pointer.hover(offset);
expect(find.byType(RoundedTextButton), findsOneWidget);
});
});
}

View File

@ -0,0 +1,108 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/mock/mock_openai_repository.dart';
import 'util/util.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:appflowy/startup/startup.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
group('integration tests for open-ai smart menu', () {
setUpAll(() async => await service.setUpAll());
setUp(() async => await service.setUp());
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 10),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 84),
),
);
});
testWidgets('testing selection on open-ai smart menu insert', (tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 0),
end: Position(path: [1], offset: 5),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [2], offset: 0),
end: Position(path: [3], offset: 0),
),
);
});
});
}
Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
await tester.initializeAppFlowy();
await mockOpenAIRepository();
await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
await tester.pumpAndSettle();
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
return (tester.state(editor).widget as AppFlowyEditor);
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
);
return;
}

View File

@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
import 'board_test.dart' as board_test; import 'board_test.dart' as board_test;
import 'switch_folder_test.dart' as switch_folder_test; import 'switch_folder_test.dart' as switch_folder_test;
import 'empty_document_test.dart' as empty_document_test; import 'empty_document_test.dart' as empty_document_test;
import 'open_ai_smart_menu_test.dart' as smart_menu_test;
/// The main task runner for all integration tests in AppFlowy. /// The main task runner for all integration tests in AppFlowy.
/// ///
@ -16,4 +17,5 @@ void main() {
switch_folder_test.main(); switch_folder_test.main();
board_test.main(); board_test.main();
empty_document_test.main(); empty_document_test.main();
smart_menu_test.main();
} }

View File

@ -9,7 +9,9 @@ import 'package:shared_preferences/shared_preferences.dart';
enum TestWorkspace { enum TestWorkspace {
board("board"), board("board"),
emptyDocument("empty_document"); emptyDocument("empty_document"),
aiWorkSpace("ai_workspace"),
coverImage("cover_image");
const TestWorkspace(this._name); const TestWorkspace(this._name);

View File

@ -0,0 +1,76 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:mocktail/mocktail.dart';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
class MyMockClient extends Mock implements http.Client {
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final requestType = request.method;
final requestUri = request.url;
if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
final responseBody = Stream.fromIterable([
utf8.encode(
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
),
utf8.encode('\n'),
utf8.encode('[DONE]'),
]);
// Return a mocked response with the expected data
return http.StreamedResponse(responseBody, 200, headers: responseHeaders);
}
// Return an error response for any other request
return http.StreamedResponse(const Stream.empty(), 404);
}
}
class MockOpenAIRepository extends HttpOpenAIRepository {
MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient());
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
final response = await client.send(request);
var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
await onStart();
final data = chunk.trim().split('data: ');
if (data[0] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
json.decode(data[0]),
);
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
if (text == previousSyntax && text == '\n') {
continue;
}
await onProcess(response);
previousSyntax = response.choices.first.text;
}
} else {
await onEnd();
}
}
}
return;
}
}

View File

@ -1 +1,2 @@
export 'target_platform.dart'; export 'target_platform.dart';
export 'url_validator.dart';

View File

@ -0,0 +1,21 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'url_validator.freezed.dart';
Either<UriFailure, Uri> parseValidUrl(String url) {
try {
final uri = Uri.parse(url);
if (uri.scheme.isEmpty || uri.host.isEmpty) {
return left(const UriFailure.invalidSchemeHost());
}
return right(uri);
} on FormatException {
return left(const UriFailure.invalidUriFormat());
}
}
@freezed
class UriFailure with _$UriFailure {
const factory UriFailure.invalidSchemeHost() = _InvalidSchemeHost;
const factory UriFailure.invalidUriFormat() = _InvalidUriFormat;
}

View File

@ -77,10 +77,7 @@ class CellController<T, D> extends Equatable {
_cellListener?.start( _cellListener?.start(
onCellChanged: (result) { onCellChanged: (result) {
result.fold( result.fold(
(_) { (_) => _loadData(),
_cellCache.remove(_cacheKey);
_loadData();
},
(err) => Log.error(err), (err) => Log.error(err),
); );
}, },
@ -174,8 +171,8 @@ class CellController<T, D> extends Equatable {
void _loadData() { void _loadData() {
_saveDataOperation?.cancel(); _saveDataOperation?.cancel();
_loadDataOperation?.cancel(); _loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () { _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
_cellDataLoader.loadData().then((data) { _cellDataLoader.loadData().then((data) {
if (data != null) { if (data != null) {
@ -183,7 +180,6 @@ class CellController<T, D> extends Equatable {
} else { } else {
_cellCache.remove(_cacheKey); _cellCache.remove(_cacheKey);
} }
_cellDataNotifier?.value = data; _cellDataNotifier?.value = data;
}); });
}); });

View File

@ -55,7 +55,7 @@ class CellControllerBuilder {
case FieldType.Number: case FieldType.Number:
final cellDataLoader = CellDataLoader( final cellDataLoader = CellDataLoader(
cellId: _cellId, cellId: _cellId,
parser: StringCellDataParser(), parser: NumberCellDataParser(),
reloadOnFieldChanged: true, reloadOnFieldChanged: true,
); );
return NumberCellController( return NumberCellController(

View File

@ -27,7 +27,12 @@ class CellDataLoader<T> {
(result) => result.fold( (result) => result.fold(
(CellPB cell) { (CellPB cell) {
try { try {
// Return null the data of the cell is empty.
if (cell.data.isEmpty) {
return null;
} else {
return parser.parserData(cell.data); return parser.parserData(cell.data);
}
} catch (e, s) { } catch (e, s) {
Log.error('$parser parser cellData failed, $e'); Log.error('$parser parser cellData failed, $e');
Log.error('Stack trace \n $s'); Log.error('Stack trace \n $s');
@ -51,6 +56,13 @@ class StringCellDataParser implements CellDataParser<String> {
} }
} }
class NumberCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
return utf8.decode(data);
}
}
class DateCellDataParser implements CellDataParser<DateCellDataPB> { class DateCellDataParser implements CellDataParser<DateCellDataPB> {
@override @override
DateCellDataPB? parserData(List<int> data) { DateCellDataPB? parserData(List<int> data) {

View File

@ -45,7 +45,12 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
Future<Option<FlowyError>> save(DateCellData data) { Future<Option<FlowyError>> save(DateCellData data) {
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId); var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString(); // This is a bit of a hack. This converts the data.date which is in
// UTC to Local but actually changes the timestamp instead of just
// changing the isUtc flag
final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date; payload.date = date;
payload.isUtc = data.date.isUtc; payload.isUtc = data.date.isUtc;
payload.includeTime = data.includeTime; payload.includeTime = data.includeTime;

View File

@ -116,7 +116,7 @@ class DatabaseController {
} }
} }
void addListener({ void setListener({
DatabaseCallbacks? onDatabaseChanged, DatabaseCallbacks? onDatabaseChanged,
LayoutCallbacks? onLayoutChanged, LayoutCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged, GroupCallbacks? onGroupChanged,
@ -212,6 +212,11 @@ class DatabaseController {
await _databaseViewBackendSvc.closeView(); await _databaseViewBackendSvc.closeView();
await fieldController.dispose(); await fieldController.dispose();
await groupListener.stop(); await groupListener.stop();
await _viewCache.dispose();
_databaseCallbacks = null;
_groupCallbacks = null;
_layoutCallbacks = null;
_calendarLayoutCallbacks = null;
} }
Future<void> _loadGroups() async { Future<void> _loadGroups() async {
@ -252,7 +257,7 @@ class DatabaseController {
_databaseCallbacks?.onRowsCreated?.call(ids); _databaseCallbacks?.onRowsCreated?.call(ids);
}, },
); );
_viewCache.addListener(callbacks); _viewCache.setListener(callbacks);
} }
void _listenOnFieldsChanged() { void _listenOnFieldsChanged() {
@ -337,9 +342,10 @@ class RowDataBuilder {
_cellDataByFieldId[fieldInfo.field.id] = num.toString(); _cellDataByFieldId[fieldInfo.field.id] = num.toString();
} }
/// The date should use the UTC timezone. Becuase the backend uses UTC timezone to format the time string.
void insertDate(FieldInfo fieldInfo, DateTime date) { void insertDate(FieldInfo fieldInfo, DateTime date) {
assert(fieldInfo.fieldType == FieldType.DateTime); assert(fieldInfo.fieldType == FieldType.DateTime);
final timestamp = (date.millisecondsSinceEpoch ~/ 1000); final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000);
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString(); _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
} }

View File

@ -112,9 +112,10 @@ class DatabaseViewCache {
Future<void> dispose() async { Future<void> dispose() async {
await _databaseViewListener.stop(); await _databaseViewListener.stop();
await _rowCache.dispose(); await _rowCache.dispose();
_callbacks = null;
} }
void addListener(DatabaseViewCallbacks callbacks) { void setListener(DatabaseViewCallbacks callbacks) {
_callbacks = callbacks; _callbacks = callbacks;
} }
} }

View File

@ -237,7 +237,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}, },
); );
_databaseController.addListener( _databaseController.setListener(
onDatabaseChanged: onDatabaseChanged, onDatabaseChanged: onDatabaseChanged,
onGroupChanged: onGroupChanged, onGroupChanged: onGroupChanged,
); );

View File

@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget {
class _BoardContentState extends State<BoardContent> { class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager; late AppFlowyBoardScrollController scrollManager;
final cardConfiguration = CardConfiguration<String>(); final renderHook = RowCardRenderHook<String>();
final config = const AppFlowyBoardConfig( final config = const AppFlowyBoardConfig(
groupBackgroundColor: Color(0xffF7F8FC), groupBackgroundColor: Color(0xffF7F8FC),
@ -87,7 +87,7 @@ class _BoardContentState extends State<BoardContent> {
@override @override
void initState() { void initState() {
scrollManager = AppFlowyBoardScrollController(); scrollManager = AppFlowyBoardScrollController();
cardConfiguration.addSelectOptionHook((options, groupId) { renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId. // The cell should hide if the option id is equal to the groupId.
final isInGroup = final isInGroup =
options.where((element) => element.id == groupId).isNotEmpty; options.where((element) => element.id == groupId).isNotEmpty;
@ -254,15 +254,15 @@ class _BoardContentState extends State<BoardContent> {
key: ValueKey(groupItemId), key: ValueKey(groupItemId),
margin: config.cardPadding, margin: config.cardPadding,
decoration: _makeBoxDecoration(context), decoration: _makeBoxDecoration(context),
child: Card<String>( child: RowCard<String>(
row: rowPB, row: rowPB,
viewId: viewId, viewId: viewId,
rowCache: rowCache, rowCache: rowCache,
cardData: groupData.group.groupId, cardData: groupData.group.groupId,
fieldId: groupItem.fieldInfo.id, groupingFieldId: groupItem.fieldInfo.id,
isEditing: isEditing, isEditing: isEditing,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
configuration: cardConfiguration, renderHook: renderHook,
openCard: (context) => _openCard( openCard: (context) => _openCard(
viewId, viewId,
fieldController, fieldController,

View File

@ -47,7 +47,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
emit(state.copyWith(database: Some(database))); emit(state.copyWith(database: Some(database)));
}, },
didLoadAllEvents: (events) { didLoadAllEvents: (events) {
emit(state.copyWith(initialEvents: events, allEvents: events)); final calenderEvents = _calendarEventDataFromEventPBs(events);
emit(
state.copyWith(
initialEvents: calenderEvents,
allEvents: calenderEvents,
),
);
}, },
didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) { didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
_loadAllEvents(); _loadAllEvents();
@ -56,6 +62,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date, String title) async { createEvent: (DateTime date, String title) async {
await _createEvent(date, title); await _createEvent(date, title);
}, },
didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(editEvent: event),
);
},
updateCalendarLayoutSetting: updateCalendarLayoutSetting:
(CalendarLayoutSettingPB layoutSetting) async { (CalendarLayoutSettingPB layoutSetting) async {
await _updateCalendarLayoutSetting(layoutSetting); await _updateCalendarLayoutSetting(layoutSetting);
@ -63,7 +74,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) { didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
var allEvents = [...state.allEvents]; var allEvents = [...state.allEvents];
final index = allEvents.indexWhere( final index = allEvents.indexWhere(
(element) => element.event!.cellId == eventData.event!.cellId, (element) => element.event!.eventId == eventData.event!.eventId,
); );
if (index != -1) { if (index != -1) {
allEvents[index] = eventData; allEvents[index] = eventData;
@ -71,22 +82,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
emit( emit(
state.copyWith( state.copyWith(
allEvents: allEvents, allEvents: allEvents,
updateEvent: eventData,
),
);
},
didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
newEvent: event,
), ),
); );
}, },
didDeleteEvents: (List<RowId> deletedRowIds) { didDeleteEvents: (List<RowId> deletedRowIds) {
var events = [...state.allEvents]; var events = [...state.allEvents];
events.retainWhere( events.retainWhere(
(element) => !deletedRowIds.contains(element.event!.cellId.rowId), (element) => !deletedRowIds.contains(element.event!.eventId),
); );
emit( emit(
state.copyWith( state.copyWith(
@ -95,9 +97,23 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
), ),
); );
}, },
didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
newEvent: event,
),
); );
}, },
); );
},
);
}
@override
Future<void> close() async {
await _databaseController.dispose();
return super.close();
} }
FieldInfo? _getCalendarFieldInfo(String fieldId) { FieldInfo? _getCalendarFieldInfo(String fieldId) {
@ -143,17 +159,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
final dateField = _getCalendarFieldInfo(settings.fieldId); final dateField = _getCalendarFieldInfo(settings.fieldId);
final titleField = _getTitleFieldInfo(); final titleField = _getTitleFieldInfo();
if (dateField != null && titleField != null) { if (dateField != null && titleField != null) {
final result = await _databaseController.createRow( final newRow = await _databaseController.createRow(
withCells: (builder) { withCells: (builder) {
builder.insertDate(dateField, date); builder.insertDate(dateField, date);
builder.insertText(titleField, title); builder.insertText(titleField, title);
}, },
).then(
(result) => result.fold(
(newRow) => newRow,
(err) {
Log.error(err);
return null;
},
),
); );
return result.fold( if (newRow != null) {
(newRow) {}, final event = await _loadEvent(newRow.id);
(err) => Log.error(err), if (event != null && !isClosed) {
); add(CalendarEvent.didCreateEvent(event));
}
}
} }
}, },
); );
@ -187,15 +213,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
result.fold( result.fold(
(events) { (events) {
if (!isClosed) { if (!isClosed) {
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[]; add(CalendarEvent.didLoadAllEvents(events.items));
for (final eventPB in events.items) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
calendarEvents.add(calendarEvent);
}
}
add(CalendarEvent.didLoadAllEvents(calendarEvents));
} }
}, },
(r) => Log.error(r), (r) => Log.error(r),
@ -203,22 +221,32 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}); });
} }
List<CalendarEventData<CalendarDayEvent>> _calendarEventDataFromEventPBs(
List<CalendarEventPB> eventPBs,
) {
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
for (final eventPB in eventPBs) {
final event = _calendarEventDataFromEventPB(eventPB);
if (event != null) {
calendarEvents.add(event);
}
}
return calendarEvents;
}
CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB( CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
CalendarEventPB eventPB, CalendarEventPB eventPB,
) { ) {
final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId]; final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];
if (fieldInfo != null) { if (fieldInfo != null) {
final cellId = CellIdentifier(
viewId: viewId,
rowId: eventPB.rowId,
fieldInfo: fieldInfo,
);
final eventData = CalendarDayEvent( final eventData = CalendarDayEvent(
event: eventPB, event: eventPB,
cellId: cellId, eventId: eventPB.rowId,
dateFieldId: eventPB.dateFieldId,
); );
// The timestamp is using UTC in the backend, so we need to convert it
// to local time.
final date = DateTime.fromMillisecondsSinceEpoch( final date = DateTime.fromMillisecondsSinceEpoch(
eventPB.timestamp.toInt() * 1000, eventPB.timestamp.toInt() * 1000,
); );
@ -243,27 +271,31 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
}; };
}, },
onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}), onRowsCreated: ((rowIds) async {
onRowsCreated: ((ids) async { for (final id in rowIds) {
for (final id in ids) {
final event = await _loadEvent(id); final event = await _loadEvent(id);
if (event != null && !isClosed) { if (event != null && !isClosed) {
add(CalendarEvent.didReceiveNewEvent(event)); add(CalendarEvent.didReceiveEvent(event));
} }
} }
}), }),
onRowsDeleted: (ids) { onRowsDeleted: (rowIds) {
if (isClosed) return; if (isClosed) return;
add(CalendarEvent.didDeleteEvents(ids)); add(CalendarEvent.didDeleteEvents(rowIds));
}, },
onRowsUpdated: (ids) async { onRowsUpdated: (rowIds) async {
if (isClosed) return; if (isClosed) return;
for (final id in ids) { for (final id in rowIds) {
final event = await _loadEvent(id); final event = await _loadEvent(id);
if (event != null) { if (event != null && isEventDayChanged(event)) {
if (isEventDayChanged(event)) {
add(CalendarEvent.didDeleteEvents([id]));
add(CalendarEvent.didReceiveEvent(event));
} else {
add(CalendarEvent.didUpdateEvent(event)); add(CalendarEvent.didUpdateEvent(event));
} }
} }
}
}, },
); );
@ -276,7 +308,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
onCalendarLayoutChanged: _didReceiveNewLayoutField, onCalendarLayoutChanged: _didReceiveNewLayoutField,
); );
_databaseController.addListener( _databaseController.setListener(
onDatabaseChanged: onDatabaseChanged, onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged, onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged, onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
@ -296,6 +328,19 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar)); add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
} }
} }
bool isEventDayChanged(
CalendarEventData<CalendarDayEvent> event,
) {
final index = state.allEvents.indexWhere(
(element) => element.event!.eventId == event.event!.eventId,
);
if (index != -1) {
return state.allEvents[index].date.day != event.date.day;
} else {
return false;
}
}
} }
typedef Events = List<CalendarEventData<CalendarDayEvent>>; typedef Events = List<CalendarEventData<CalendarDayEvent>>;
@ -310,7 +355,7 @@ class CalendarEvent with _$CalendarEvent {
) = _ReceiveCalendarSettings; ) = _ReceiveCalendarSettings;
// Called after loading all the current evnets // Called after loading all the current evnets
const factory CalendarEvent.didLoadAllEvents(Events events) = const factory CalendarEvent.didLoadAllEvents(List<CalendarEventPB> events) =
_ReceiveCalendarEvents; _ReceiveCalendarEvents;
// Called when specific event was updated // Called when specific event was updated
@ -319,10 +364,15 @@ class CalendarEvent with _$CalendarEvent {
) = _DidUpdateEvent; ) = _DidUpdateEvent;
// Called after creating a new event // Called after creating a new event
const factory CalendarEvent.didReceiveNewEvent( const factory CalendarEvent.didCreateEvent(
CalendarEventData<CalendarDayEvent> event, CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveNewEvent; ) = _DidReceiveNewEvent;
// Called when receive a new event
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveEvent;
// Called when deleting events // Called when deleting events
const factory CalendarEvent.didDeleteEvents(List<RowId> rowIds) = const factory CalendarEvent.didDeleteEvents(List<RowId> rowIds) =
_DidDeleteEvents; _DidDeleteEvents;
@ -348,11 +398,13 @@ class CalendarEvent with _$CalendarEvent {
class CalendarState with _$CalendarState { class CalendarState with _$CalendarState {
const factory CalendarState({ const factory CalendarState({
required Option<DatabasePB> database, required Option<DatabasePB> database,
// events by row id
required Events allEvents, required Events allEvents,
required Events initialEvents, required Events initialEvents,
CalendarEventData<CalendarDayEvent>? editEvent,
CalendarEventData<CalendarDayEvent>? newEvent, CalendarEventData<CalendarDayEvent>? newEvent,
required List<RowId> deleteEventIds,
CalendarEventData<CalendarDayEvent>? updateEvent, CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
required Option<CalendarLayoutSettingPB> settings, required Option<CalendarLayoutSettingPB> settings,
required DatabaseLoadingState loadingState, required DatabaseLoadingState loadingState,
required Option<FlowyError> noneOrError, required Option<FlowyError> noneOrError,
@ -389,8 +441,12 @@ class CalendarEditingRow {
class CalendarDayEvent { class CalendarDayEvent {
final CalendarEventPB event; final CalendarEventPB event;
final CellIdentifier cellId; final String dateFieldId;
final String eventId;
RowId get eventId => cellId.rowId; CalendarDayEvent({
CalendarDayEvent({required this.cellId, required this.event}); required this.dateFieldId,
required this.eventId,
required this.event,
});
} }

View File

@ -1,7 +1,10 @@
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart'; import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@ -10,11 +13,11 @@ import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_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/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../grid/presentation/layout/sizes.dart'; import '../../grid/presentation/layout/sizes.dart';
import '../../widgets/row/cells/select_option_cell/extension.dart';
import '../application/calendar_bloc.dart'; import '../application/calendar_bloc.dart';
class CalendarDayCard extends StatelessWidget { class CalendarDayCard extends StatelessWidget {
@ -23,11 +26,10 @@ class CalendarDayCard extends StatelessWidget {
final bool isInMonth; final bool isInMonth;
final DateTime date; final DateTime date;
final RowCache _rowCache; final RowCache _rowCache;
final CardCellBuilder _cellBuilder;
final List<CalendarDayEvent> events; final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent; final void Function(DateTime) onCreateEvent;
CalendarDayCard({ const CalendarDayCard({
required this.viewId, required this.viewId,
required this.isToday, required this.isToday,
required this.isInMonth, required this.isInMonth,
@ -37,7 +39,6 @@ class CalendarDayCard extends StatelessWidget {
required this.events, required this.events,
Key? key, Key? key,
}) : _rowCache = rowCache, }) : _rowCache = rowCache,
_cellBuilder = CardCellBuilder(rowCache.cellCache),
super(key: key); super(key: key);
@override @override
@ -49,65 +50,183 @@ class CalendarDayCard extends StatelessWidget {
return ChangeNotifierProvider( return ChangeNotifierProvider(
create: (_) => _CardEnterNotifier(), create: (_) => _CardEnterNotifier(),
builder: ((context, child) { builder: (context, child) {
final children = events.map((event) { Widget? multipleCards;
return _DayEventCell( if (events.isNotEmpty) {
event: event, multipleCards = Flexible(
viewId: viewId, child: ListView.separated(
onClick: () => _showRowDetailPage(event, context), itemBuilder: (BuildContext context, int index) =>
child: _cellBuilder.buildCell( _buildCard(context, events[index]),
cellId: event.cellId, itemCount: events.length,
styles: {FieldType.RichText: TextCardCellStyle(10)}, padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
), ),
); );
}).toList(); }
final child = Column( final child = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( _Header(
padding: const EdgeInsets.all(8.0),
child: _Header(
date: date, date: date,
isInMonth: isInMonth, isInMonth: isInMonth,
isToday: isToday, isToday: isToday,
onCreate: () => onCreateEvent(date), onCreate: () => onCreateEvent(date),
), ),
),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight), VSpace(GridSize.typeOptionSeparatorHeight),
Flexible(
child: ListView.separated( // Use SizedBox instead of ListView if there are no cards.
itemBuilder: (BuildContext context, int index) { multipleCards ?? const SizedBox(),
return children[index];
},
itemCount: children.length,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
),
),
], ],
); );
return Container( return Container(
color: backgroundColor, color: backgroundColor,
child: GestureDetector(
onDoubleTap: () => onCreateEvent(date),
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.basic,
onEnter: (p) => notifyEnter(context, true), onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false), onExit: (p) => notifyEnter(context, false),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: child, child: child,
), ),
), ),
),
); );
}), },
);
}
GestureDetector _buildCard(BuildContext context, CalendarDayEvent event) {
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<String>(
_rowCache.cellCache,
styles: styles,
);
final rowInfo = _rowCache.getRow(event.eventId);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, primaryFieldId, _) {
if (cellData.isEmpty) {
return const SizedBox();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
cellData,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
),
);
});
renderHook.addDateCellHook((cellData, cardData, _) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: FlowyText.regular(
cellData.date,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: FlowyText.regular(
cellData.time,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
)
],
),
),
);
});
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
final children = selectedOptions.map(
(option) {
return SelectOptionTag.fromOption(
context: context,
option: option,
);
},
).toList();
return IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: SizedBox.expand(
child: Wrap(spacing: 4, runSpacing: 4, children: children),
),
),
);
});
// renderHook.addDateFieldHook((cellData, cardData) {
final card = RowCard<String>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
row: rowInfo!.rowPB,
viewId: viewId,
rowCache: _rowCache,
cardData: event.dateFieldId,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => _showRowDetailPage(event, context),
styleConfiguration: const RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
),
renderHook: renderHook,
onStartEditing: () {},
onEndEditing: () {},
);
return GestureDetector(
onTap: () => _showRowDetailPage(event, context),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.5,
),
),
borderRadius: Corners.s6Border,
),
child: card,
),
),
); );
} }
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) { void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController( final dataController = RowController(
rowId: event.cellId.rowId, rowId: event.eventId,
viewId: viewId, viewId: viewId,
rowCache: _rowCache, rowCache: _rowCache,
); );
@ -133,42 +252,6 @@ class CalendarDayCard extends StatelessWidget {
} }
} }
class _DayEventCell extends StatelessWidget {
final String viewId;
final CalendarDayEvent event;
final VoidCallback onClick;
final Widget child;
const _DayEventCell({
required this.viewId,
required this.event,
required this.onClick,
required this.child,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyHover(
child: GestureDetector(
onTap: onClick,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
),
),
borderRadius: Corners.s6Border,
),
child: child,
),
),
);
}
}
class _Header extends StatelessWidget { class _Header extends StatelessWidget {
final bool isToday; final bool isToday;
final bool isInMonth; final bool isInMonth;
@ -191,12 +274,16 @@ class _Header extends StatelessWidget {
isInMonth: isInMonth, isInMonth: isInMonth,
date: date, date: date,
); );
return Row(
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [ children: [
if (notifier.onEnter) _NewEventButton(onClick: onCreate), if (notifier.onEnter) _NewEventButton(onClick: onCreate),
const Spacer(), const Spacer(),
badge, badge,
], ],
),
); );
}, },
); );
@ -215,10 +302,8 @@ class _NewEventButton extends StatelessWidget {
return FlowyIconButton( return FlowyIconButton(
onPressed: onClick, onPressed: onClick,
iconPadding: EdgeInsets.zero, iconPadding: EdgeInsets.zero,
icon: svgWidget( icon: const FlowySvg(name: "home/add"),
"home/add", hoverColor: AFThemeExtension.of(context).lightGreyHover,
color: Theme.of(context).iconTheme.color,
),
width: 22, width: 22,
); );
} }
@ -237,31 +322,38 @@ class _DayBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onSurface; Color dayTextColor = Theme.of(context).colorScheme.onBackground;
String dayString = date.day == 1 String monthString =
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date) DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
: date.day.toString(); String dayString = date.day.toString();
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
if (!isInMonth) { if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor; dayTextColor = Theme.of(context).disabledColor;
} }
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
Widget day = Container( return Row(
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null, color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border, borderRadius: Corners.s6Border,
), ),
width: isToday ? 26 : null,
height: isToday ? 26 : null,
padding: GridSize.typeOptionContentInsets, padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium( child: FlowyText.medium(
dayString, dayString,
color: dayTextColor, color: dayTextColor,
), ),
),
),
],
); );
return day;
} }
} }

View File

@ -9,6 +9,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/row/row_data_controller.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart'; import 'calendar_day.dart';
import 'layout/sizes.dart'; import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart'; import 'toolbar/calendar_toolbar.dart';
@ -70,19 +73,16 @@ class _CalendarPageState extends State<CalendarPage> {
}, },
), ),
BlocListener<CalendarBloc, CalendarState>( BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.updateEvent != c.updateEvent, listenWhen: (p, c) => p.editEvent != c.editEvent,
listener: (context, state) { listener: (context, state) {
if (state.updateEvent != null) { if (state.editEvent != null) {
_eventController.removeWhere( _showEditEventPage(state.editEvent!.event!, context);
(element) =>
state.updateEvent!.event!.eventId ==
element.event!.eventId,
);
_eventController.add(state.updateEvent!);
} }
}, },
), ),
BlocListener<CalendarBloc, CalendarState>( BlocListener<CalendarBloc, CalendarState>(
// Event create by click the + button or double click on the
// calendar
listenWhen: (p, c) => p.newEvent != c.newEvent, listenWhen: (p, c) => p.newEvent != c.newEvent,
listener: (context, state) { listener: (context, state) {
if (state.newEvent != null) { if (state.newEvent != null) {
@ -116,7 +116,7 @@ class _CalendarPageState extends State<CalendarPage> {
child: MonthView( child: MonthView(
key: _calendarState, key: _calendarState,
controller: _eventController, controller: _eventController,
cellAspectRatio: .9, cellAspectRatio: .6,
startDay: _weekdayFromInt(firstDayOfWeek), startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor, borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder, headerBuilder: _headerNavigatorBuilder,
@ -137,7 +137,7 @@ class _CalendarPageState extends State<CalendarPage> {
FlowyIconButton( FlowyIconButton(
width: CalendarSize.navigatorButtonWidth, width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight, height: CalendarSize.navigatorButtonHeight,
icon: svgWidget('home/arrow_left'), icon: const FlowySvg(name: 'home/arrow_left'),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(), tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(), onPressed: () => _calendarState?.currentState?.previousPage(),
@ -155,7 +155,7 @@ class _CalendarPageState extends State<CalendarPage> {
FlowyIconButton( FlowyIconButton(
width: CalendarSize.navigatorButtonWidth, width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight, height: CalendarSize.navigatorButtonHeight,
icon: svgWidget('home/arrow_right'), icon: const FlowySvg(name: 'home/arrow_right'),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(), tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(), onPressed: () => _calendarState?.currentState?.nextPage(),
@ -185,7 +185,12 @@ class _CalendarPageState extends State<CalendarPage> {
isInMonth, isInMonth,
) { ) {
final events = calenderEvents.map((value) => value.event!).toList(); final events = calenderEvents.map((value) => value.event!).toList();
// Sort the events by timestamp. Because the database view is not
// reserving the order of the events. Reserving the order of the rows/events
// is implemnted in the develop branch(WIP). Will be replaced with that.
events.sort(
(a, b) => a.event.timestamp.compareTo(b.event.timestamp),
);
return CalendarDayCard( return CalendarDayCard(
viewId: widget.view.id, viewId: widget.view.id,
isToday: isToday, isToday: isToday,
@ -208,4 +213,24 @@ class _CalendarPageState extends State<CalendarPage> {
// MonthView places the first day of week on the second column for some reason. // MonthView places the first day of week on the second column for some reason.
return WeekDays.values[(dayOfWeek + 1) % 7]; return WeekDays.values[(dayOfWeek + 1) % 7];
} }
void _showEditEventPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.eventId,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: _calendarBloc.rowCache.cellCache,
),
dataController: dataController,
);
},
);
}
} }

View File

@ -87,7 +87,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
} }
}, },
); );
databaseController.addListener(onDatabaseChanged: onDatabaseChanged); databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
} }
Future<void> _openGrid(Emitter<GridState> emit) async { Future<void> _openGrid(Emitter<GridState> emit) async {

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -7,31 +8,39 @@ import '../../../application/row/row_data_controller.dart';
part 'row_detail_bloc.freezed.dart'; part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> { class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final RowBackendService rowService;
final RowController dataController; final RowController dataController;
RowDetailBloc({ RowDetailBloc({
required this.dataController, required this.dataController,
}) : super(RowDetailState.initial()) { }) : rowService = RowBackendService(viewId: dataController.viewId),
super(RowDetailState.initial()) {
on<RowDetailEvent>( on<RowDetailEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.when(
initial: (_Initial value) async { initial: () async {
await _startListening(); await _startListening();
final cells = dataController.loadData(); final cells = dataController.loadData();
if (!isClosed) { if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
} }
}, },
didReceiveCellDatas: (_DidReceiveCellDatas value) { didReceiveCellDatas: (cells) {
emit(state.copyWith(gridCells: value.gridCells)); emit(state.copyWith(gridCells: cells));
}, },
deleteField: (_DeleteField value) { deleteField: (fieldId) {
final fieldService = FieldBackendService( final fieldService = FieldBackendService(
viewId: dataController.viewId, viewId: dataController.viewId,
fieldId: value.fieldId, fieldId: fieldId,
); );
fieldService.deleteField(); fieldService.deleteField();
}, },
deleteRow: (rowId) async {
await rowService.deleteRow(rowId);
},
duplicateRow: (String rowId) async {
await rowService.duplicateRow(rowId);
},
); );
}, },
); );
@ -58,6 +67,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
class RowDetailEvent with _$RowDetailEvent { class RowDetailEvent with _$RowDetailEvent {
const factory RowDetailEvent.initial() = _Initial; const factory RowDetailEvent.initial() = _Initial;
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField; const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
const factory RowDetailEvent.duplicateRow(String rowId) = _DuplicateRow;
const factory RowDetailEvent.didReceiveCellDatas( const factory RowDetailEvent.didReceiveCellDatas(
List<CellIdentifier> gridCells, List<CellIdentifier> gridCells,
) = _DidReceiveCellDatas; ) = _DidReceiveCellDatas;

View File

@ -147,8 +147,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
widget.popoverMutex.listenOnPopoverChanged(() { widget.popoverMutex.listenOnPopoverChanged(() {
if (focusNode.hasFocus) { if (focusNode.hasFocus) {
focusNode.unfocus(); focusNode.unfocus();
} else {
focusNode.requestFocus();
} }
}); });
@ -205,6 +203,7 @@ class _DeleteFieldButton extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
final enable = !state.canDelete && !state.isGroupField; final enable = !state.canDelete && !state.isGroupField;
Widget button = FlowyButton( Widget button = FlowyButton(
disable: !enable,
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_field_delete.tr(), LocaleKeys.grid_field_delete.tr(),
color: enable ? null : Theme.of(context).disabledColor, color: enable ? null : Theme.of(context).disabledColor,

View File

@ -1,6 +1,5 @@
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -58,15 +57,12 @@ class FieldTypeCell extends StatelessWidget {
return SizedBox( return SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
fieldType.title(), fieldType.title(),
color: AFThemeExtension.of(context).textColor,
), ),
onTap: () => onSelectField(fieldType), onTap: () => onSelectField(fieldType),
leftIcon: svgWidget( leftIcon: FlowySvg(
fieldType.iconName(), name: fieldType.iconName(),
color: Theme.of(context).iconTheme.color,
), ),
), ),
); );

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either; import 'package:dartz/dartz.dart' show Either;
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@ -113,20 +112,12 @@ class _SwitchFieldButton extends StatelessWidget {
Widget _buildMoreButton(BuildContext context) { Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldTypeOptionEditBloc>(); final bloc = context.read<FieldTypeOptionEditBloc>();
return FlowyButton( return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
bloc.state.field.fieldType.title(), bloc.state.field.fieldType.title(),
color: AFThemeExtension.of(context).textColor,
), ),
margin: GridSize.typeOptionContentInsets, margin: GridSize.typeOptionContentInsets,
leftIcon: svgWidget( leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
bloc.state.field.fieldType.iconName(), rightIcon: const FlowySvg(name: 'grid/more'),
color: Theme.of(context).iconTheme.color,
),
rightIcon: svgWidget(
"grid/more",
color: Theme.of(context).iconTheme.color,
),
); );
} }
} }

View File

@ -186,6 +186,7 @@ class CreateFieldButton extends StatelessWidget {
return AppFlowyPopover( return AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned, direction: PopoverDirection.bottomWithRightAligned,
asBarrier: true, asBarrier: true,
margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(240, 600)), constraints: BoxConstraints.loose(const Size(240, 600)),
child: FlowyButton( child: FlowyButton(
radius: BorderRadius.zero, radius: BorderRadius.zero,

View File

@ -1,7 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart' hide DateFormat; import 'package:easy_localization/easy_localization.dart' hide DateFormat;
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
@ -54,7 +52,6 @@ class DateTypeOptionWidget extends TypeOptionWidget {
const TypeOptionSeparator(), const TypeOptionSeparator(),
_renderDateFormatButton(context, state.typeOption.dateFormat), _renderDateFormatButton(context, state.typeOption.dateFormat),
_renderTimeFormatButton(context, state.typeOption.timeFormat), _renderTimeFormatButton(context, state.typeOption.timeFormat),
const _IncludeTimeButton(),
]; ];
return ListView.separated( return ListView.separated(
@ -191,44 +188,6 @@ class TimeFormatButton extends StatelessWidget {
} }
} }
class _IncludeTimeButton extends StatelessWidget {
const _IncludeTimeButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
selector: (state) => state.typeOption.includeTime,
builder: (context, includeTime) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: Row(
children: [
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
const Spacer(),
Toggle(
value: includeTime,
onChanged: (value) {
context
.read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.includeTime(!value));
},
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
),
);
},
);
}
}
class DateFormatList extends StatelessWidget { class DateFormatList extends StatelessWidget {
final DateFormatPB selectedFormat; final DateFormatPB selectedFormat;
final Function(DateFormatPB format) onSelected; final Function(DateFormatPB format) onSelected;
@ -280,7 +239,7 @@ class DateFormatCell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? checkmark; Widget? checkmark;
if (isSelected) { if (isSelected) {
checkmark = svgWidget("grid/checkmark"); checkmark = const FlowySvg(name: 'grid/checkmark');
} }
return SizedBox( return SizedBox(
@ -364,7 +323,7 @@ class TimeFormatCell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? checkmark; Widget? checkmark;
if (isSelected) { if (isSelected) {
checkmark = svgWidget("grid/checkmark"); checkmark = const FlowySvg(name: 'grid/checkmark');
} }
return SizedBox( return SizedBox(

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -60,15 +59,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
final selectNumUnitButton = SizedBox( final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
margin: GridSize.typeOptionContentInsets, margin: GridSize.typeOptionContentInsets,
rightIcon: svgWidget( rightIcon: const FlowySvg(name: 'grid/more'),
"grid/more",
color: AFThemeExtension.of(context).textColor,
),
text: FlowyText.regular( text: FlowyText.regular(
state.typeOption.format.title(), state.typeOption.format.title(),
color: AFThemeExtension.of(context).textColor,
), ),
), ),
); );
@ -79,7 +73,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyText.medium( child: FlowyText.medium(
LocaleKeys.grid_field_numberFormat.tr(), LocaleKeys.grid_field_numberFormat.tr(),
color: AFThemeExtension.of(context).textColor,
), ),
); );
return Padding( return Padding(
@ -188,7 +181,9 @@ class NumberFormatCell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? checkmark; Widget? checkmark;
if (isSelected) { if (isSelected) {
checkmark = svgWidget("grid/checkmark"); checkmark = const FlowySvg(
name: 'grid/checkmark',
);
} }
return SizedBox( return SizedBox(

View File

@ -105,15 +105,10 @@ class _DeleteTag extends StatelessWidget {
return SizedBox( return SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_selectOption_deleteTag.tr(), LocaleKeys.grid_selectOption_deleteTag.tr(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: svgWidget(
"grid/delete",
color: Theme.of(context).iconTheme.color,
), ),
leftIcon: const FlowySvg(name: 'grid/delete'),
onTap: () { onTap: () {
context context
.read<EditSelectOptionBloc>() .read<EditSelectOptionBloc>()
@ -226,7 +221,11 @@ class _SelectOptionColorCell extends StatelessWidget {
return SizedBox( return SizedBox(
height: GridSize.popoverItemHeight, height: GridSize.popoverItemHeight,
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(color.optionName()), hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
color.optionName(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: colorIcon, leftIcon: colorIcon,
rightIcon: checkmark, rightIcon: checkmark,
onTap: () { onTap: () {

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
@ -13,23 +14,40 @@ import 'card_cell_builder.dart';
import 'container/accessory.dart'; import 'container/accessory.dart';
import 'container/card_container.dart'; import 'container/card_container.dart';
class Card<CustomCardData> extends StatefulWidget { /// Edit a database row with card style widget
class RowCard<CustomCardData> extends StatefulWidget {
final RowPB row; final RowPB row;
final String viewId; final String viewId;
final String fieldId; final String? groupingFieldId;
/// Allows passing a custom card data object to the card. The card will be
/// returned in the [CardCellBuilder] and can be used to build the card.
final CustomCardData? cardData; final CustomCardData? cardData;
final bool isEditing; final bool isEditing;
final RowCache rowCache; final RowCache rowCache;
final CardCellBuilder<CustomCardData> cellBuilder;
final void Function(BuildContext) openCard;
final VoidCallback onStartEditing;
final VoidCallback onEndEditing;
final CardConfiguration<CustomCardData>? configuration;
const Card({ /// The [CardCellBuilder] is used to build the card cells.
final CardCellBuilder<CustomCardData> cellBuilder;
/// Called when the user taps on the card.
final void Function(BuildContext) openCard;
/// Called when the user starts editing the card.
final VoidCallback onStartEditing;
/// Called when the user ends editing the card.
final VoidCallback onEndEditing;
/// The [RowCardRenderHook] is used to render the card's cell. Other than
/// using the default cell builder. For example the [SelectOptionCardCell]
final RowCardRenderHook<CustomCardData>? renderHook;
final RowCardStyleConfiguration styleConfiguration;
const RowCard({
required this.row, required this.row,
required this.viewId, required this.viewId,
required this.fieldId, this.groupingFieldId,
required this.isEditing, required this.isEditing,
required this.rowCache, required this.rowCache,
required this.cellBuilder, required this.cellBuilder,
@ -37,15 +55,19 @@ class Card<CustomCardData> extends StatefulWidget {
required this.onStartEditing, required this.onStartEditing,
required this.onEndEditing, required this.onEndEditing,
this.cardData, this.cardData,
this.configuration, this.styleConfiguration = const RowCardStyleConfiguration(
showAccessory: true,
),
this.renderHook,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
State<Card<CustomCardData>> createState() => _CardState<CustomCardData>(); State<RowCard<CustomCardData>> createState() =>
_RowCardState<CustomCardData>();
} }
class _CardState<T> extends State<Card<T>> { class _RowCardState<T> extends State<RowCard<T>> {
late CardBloc _cardBloc; late CardBloc _cardBloc;
late EditableRowNotifier rowNotifier; late EditableRowNotifier rowNotifier;
late PopoverController popoverController; late PopoverController popoverController;
@ -56,15 +78,15 @@ class _CardState<T> extends State<Card<T>> {
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing); rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
_cardBloc = CardBloc( _cardBloc = CardBloc(
viewId: widget.viewId, viewId: widget.viewId,
groupFieldId: widget.fieldId, groupFieldId: widget.groupingFieldId,
isEditing: widget.isEditing, isEditing: widget.isEditing,
row: widget.row, row: widget.row,
rowCache: widget.rowCache, rowCache: widget.rowCache,
)..add(const BoardCardEvent.initial()); )..add(const RowCardEvent.initial());
rowNotifier.isEditing.addListener(() { rowNotifier.isEditing.addListener(() {
if (!mounted) return; if (!mounted) return;
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value)); _cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) { if (rowNotifier.isEditing.value) {
widget.onStartEditing(); widget.onStartEditing();
@ -81,7 +103,7 @@ class _CardState<T> extends State<Card<T>> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cardBloc, value: _cardBloc,
child: BlocBuilder<CardBloc, BoardCardState>( child: BlocBuilder<CardBloc, RowCardState>(
buildWhen: (previous, current) { buildWhen: (previous, current) {
// Rebuild when: // Rebuild when:
// 1.If the length of the cells is not the same // 1.If the length of the cells is not the same
@ -106,21 +128,26 @@ class _CardState<T> extends State<Card<T>> {
context, context,
popoverContext, popoverContext,
), ),
child: BoardCardContainer( child: RowCardContainer(
buildAccessoryWhen: () => state.isEditing == false, buildAccessoryWhen: () => state.isEditing == false,
accessoryBuilder: (context) { accessoryBuilder: (context) {
if (widget.styleConfiguration.showAccessory == false) {
return [];
} else {
return [ return [
_CardEditOption(rowNotifier: rowNotifier), _CardEditOption(rowNotifier: rowNotifier),
_CardMoreOption(), _CardMoreOption(),
]; ];
}
}, },
openAccessory: _handleOpenAccessory, openAccessory: _handleOpenAccessory,
openCard: (context) => widget.openCard(context), openCard: (context) => widget.openCard(context),
child: _CardContent<T>( child: _CardContent<T>(
rowNotifier: rowNotifier, rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder, cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells, cells: state.cells,
cardConfiguration: widget.configuration, renderHook: widget.renderHook,
cardData: widget.cardData, cardData: widget.cardData,
), ),
), ),
@ -166,15 +193,17 @@ class _CardState<T> extends State<Card<T>> {
class _CardContent<CustomCardData> extends StatelessWidget { class _CardContent<CustomCardData> extends StatelessWidget {
final CardCellBuilder<CustomCardData> cellBuilder; final CardCellBuilder<CustomCardData> cellBuilder;
final EditableRowNotifier rowNotifier; final EditableRowNotifier rowNotifier;
final List<BoardCellEquatable> cells; final List<CellIdentifier> cells;
final CardConfiguration<CustomCardData>? cardConfiguration; final RowCardRenderHook<CustomCardData>? renderHook;
final CustomCardData? cardData; final CustomCardData? cardData;
final RowCardStyleConfiguration styleConfiguration;
const _CardContent({ const _CardContent({
required this.rowNotifier, required this.rowNotifier,
required this.cellBuilder, required this.cellBuilder,
required this.cells, required this.cells,
required this.cardData, required this.cardData,
this.cardConfiguration, required this.styleConfiguration,
this.renderHook,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -188,30 +217,30 @@ class _CardContent<CustomCardData> extends StatelessWidget {
List<Widget> _makeCells( List<Widget> _makeCells(
BuildContext context, BuildContext context,
List<BoardCellEquatable> cells, List<CellIdentifier> cells,
) { ) {
final List<Widget> children = []; final List<Widget> children = [];
// Remove all the cell listeners. // Remove all the cell listeners.
rowNotifier.unbind(); rowNotifier.unbind();
cells.asMap().forEach( cells.asMap().forEach(
(int index, BoardCellEquatable cell) { (int index, CellIdentifier cell) {
final isEditing = index == 0 ? rowNotifier.isEditing.value : false; final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
final cellNotifier = EditableCardNotifier(isEditing: isEditing); final cellNotifier = EditableCardNotifier(isEditing: isEditing);
if (index == 0) { if (index == 0) {
// Only use the first cell to receive user's input when click the edit // Only use the first cell to receive user's input when click the edit
// button // button
rowNotifier.bindCell(cell.identifier, cellNotifier); rowNotifier.bindCell(cell, cellNotifier);
} }
final child = Padding( final child = Padding(
key: cell.identifier.key(), key: cell.key(),
padding: const EdgeInsets.only(left: 4, right: 4), padding: styleConfiguration.cellPadding,
child: cellBuilder.buildCell( child: cellBuilder.buildCell(
cellId: cell.identifier, cellId: cell,
cellNotifier: cellNotifier, cellNotifier: cellNotifier,
cardConfiguration: cardConfiguration, renderHook: renderHook,
cardData: cardData, cardData: cardData,
), ),
); );
@ -265,3 +294,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
@override @override
AccessoryType get type => AccessoryType.edit; AccessoryType get type => AccessoryType.edit;
} }
class RowCardStyleConfiguration {
final bool showAccessory;
final EdgeInsets cellPadding;
const RowCardStyleConfiguration({
this.showAccessory = true,
this.cellPadding = const EdgeInsets.only(left: 4, right: 4),
});
}

View File

@ -1,5 +1,4 @@
import 'dart:collection'; import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -12,9 +11,9 @@ import '../../application/row/row_service.dart';
part 'card_bloc.freezed.dart'; part 'card_bloc.freezed.dart';
class CardBloc extends Bloc<BoardCardEvent, BoardCardState> { class CardBloc extends Bloc<RowCardEvent, RowCardState> {
final RowPB row; final RowPB row;
final String groupFieldId; final String? groupFieldId;
final RowBackendService _rowBackendSvc; final RowBackendService _rowBackendSvc;
final RowCache _rowCache; final RowCache _rowCache;
VoidCallback? _rowCallback; VoidCallback? _rowCallback;
@ -28,13 +27,13 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
}) : _rowBackendSvc = RowBackendService(viewId: viewId), }) : _rowBackendSvc = RowBackendService(viewId: viewId),
_rowCache = rowCache, _rowCache = rowCache,
super( super(
BoardCardState.initial( RowCardState.initial(
row, row,
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)), _makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
isEditing, isEditing,
), ),
) { ) {
on<BoardCardEvent>( on<RowCardEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async { initial: () async {
@ -69,7 +68,7 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
return RowInfo( return RowInfo(
viewId: _rowBackendSvc.viewId, viewId: _rowBackendSvc.viewId,
fields: UnmodifiableListView( fields: UnmodifiableListView(
state.cells.map((cell) => cell.identifier.fieldInfo).toList(), state.cells.map((cell) => cell.fieldInfo).toList(),
), ),
rowPB: state.rowPB, rowPB: state.rowPB,
); );
@ -81,70 +80,58 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
onCellUpdated: (cellMap, reason) { onCellUpdated: (cellMap, reason) {
if (!isClosed) { if (!isClosed) {
final cells = _makeCells(groupFieldId, cellMap); final cells = _makeCells(groupFieldId, cellMap);
add(BoardCardEvent.didReceiveCells(cells, reason)); add(RowCardEvent.didReceiveCells(cells, reason));
} }
}, },
); );
} }
} }
List<BoardCellEquatable> _makeCells( List<CellIdentifier> _makeCells(
String groupFieldId, String? groupFieldId,
CellByFieldId originalCellMap, CellByFieldId originalCellMap,
) { ) {
List<BoardCellEquatable> cells = []; List<CellIdentifier> cells = [];
for (final entry in originalCellMap.entries) { for (final entry in originalCellMap.entries) {
// Filter out the cell if it's fieldId equal to the groupFieldId // Filter out the cell if it's fieldId equal to the groupFieldId
if (entry.value.fieldId != groupFieldId) { if (groupFieldId != null) {
cells.add(BoardCellEquatable(entry.value)); if (entry.value.fieldId == groupFieldId) {
continue;
} }
} }
cells.add(entry.value);
}
return cells; return cells;
} }
@freezed @freezed
class BoardCardEvent with _$BoardCardEvent { class RowCardEvent with _$RowCardEvent {
const factory BoardCardEvent.initial() = _InitialRow; const factory RowCardEvent.initial() = _InitialRow;
const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing; const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory BoardCardEvent.didReceiveCells( const factory RowCardEvent.didReceiveCells(
List<BoardCellEquatable> cells, List<CellIdentifier> cells,
RowsChangedReason reason, RowsChangedReason reason,
) = _DidReceiveCells; ) = _DidReceiveCells;
} }
@freezed @freezed
class BoardCardState with _$BoardCardState { class RowCardState with _$RowCardState {
const factory BoardCardState({ const factory RowCardState({
required RowPB rowPB, required RowPB rowPB,
required List<BoardCellEquatable> cells, required List<CellIdentifier> cells,
required bool isEditing, required bool isEditing,
RowsChangedReason? changeReason, RowsChangedReason? changeReason,
}) = _BoardCardState; }) = _RowCardState;
factory BoardCardState.initial( factory RowCardState.initial(
RowPB rowPB, RowPB rowPB,
List<BoardCellEquatable> cells, List<CellIdentifier> cells,
bool isEditing, bool isEditing,
) => ) =>
BoardCardState( RowCardState(
rowPB: rowPB, rowPB: rowPB,
cells: cells, cells: cells,
isEditing: isEditing, isEditing: isEditing,
); );
} }
class BoardCellEquatable extends Equatable {
final CellIdentifier identifier;
const BoardCellEquatable(this.identifier);
@override
List<Object?> get props {
return [
identifier.fieldInfo.id,
identifier.fieldInfo.fieldType,
identifier.fieldInfo.visibility,
identifier.fieldInfo.width,
];
}
}

View File

@ -15,15 +15,15 @@ import 'cells/url_card_cell.dart';
// T represents as the Generic card data // T represents as the Generic card data
class CardCellBuilder<CustomCardData> { class CardCellBuilder<CustomCardData> {
final CellCache cellCache; final CellCache cellCache;
final Map<FieldType, CardCellStyle>? styles;
CardCellBuilder(this.cellCache); CardCellBuilder(this.cellCache, {this.styles});
Widget buildCell({ Widget buildCell({
CustomCardData? cardData, CustomCardData? cardData,
required CellIdentifier cellId, required CellIdentifier cellId,
EditableCardNotifier? cellNotifier, EditableCardNotifier? cellNotifier,
CardConfiguration<CustomCardData>? cardConfiguration, RowCardRenderHook<CustomCardData>? renderHook,
Map<FieldType, CardCellStyle>? styles,
}) { }) {
final cellControllerBuilder = CellControllerBuilder( final cellControllerBuilder = CellControllerBuilder(
cellId: cellId, cellId: cellId,
@ -39,20 +39,21 @@ class CardCellBuilder<CustomCardData> {
key: key, key: key,
); );
case FieldType.DateTime: case FieldType.DateTime:
return DateCardCell( return DateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.SingleSelect: case FieldType.SingleSelect:
return SelectOptionCardCell<CustomCardData>( return SelectOptionCardCell<CustomCardData>(
renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect], renderHook: renderHook?.renderHook[FieldType.SingleSelect],
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
cardData: cardData, cardData: cardData,
key: key, key: key,
); );
case FieldType.MultiSelect: case FieldType.MultiSelect:
return SelectOptionCardCell<CustomCardData>( return SelectOptionCardCell<CustomCardData>(
renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect], renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
cardData: cardData, cardData: cardData,
editableNotifier: cellNotifier, editableNotifier: cellNotifier,
@ -64,19 +65,24 @@ class CardCellBuilder<CustomCardData> {
key: key, key: key,
); );
case FieldType.Number: case FieldType.Number:
return NumberCardCell( return NumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
style: isStyleOrNull<NumberCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.RichText: case FieldType.RichText:
return TextCardCell( return TextCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier, editableNotifier: cellNotifier,
cardData: cardData,
style: isStyleOrNull<TextCardCellStyle>(style), style: isStyleOrNull<TextCardCellStyle>(style),
key: key, key: key,
); );
case FieldType.URL: case FieldType.URL:
return URLCardCell( return URLCardCell<CustomCardData>(
style: isStyleOrNull<URLCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );

View File

@ -1,27 +1,72 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
typedef CellRenderHook<C, T> = Widget? Function(C cellData, T cardData); typedef CellRenderHook<C, CustomCardData> = Widget? Function(
C cellData,
CustomCardData cardData,
BuildContext buildContext,
);
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>; typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
class CardConfiguration<CustomCardData> { /// The [RowCardRenderHook] is used to customize the rendering of the
/// card cell. Each cell has itw own field type. So the [renderHook]
/// is a map of [FieldType] to [CellRenderHook].
class RowCardRenderHook<CustomCardData> {
final RenderHookByFieldType<CustomCardData> renderHook = {}; final RenderHookByFieldType<CustomCardData> renderHook = {};
CardConfiguration(); RowCardRenderHook();
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
void addSelectOptionHook( void addSelectOptionHook(
CellRenderHook<List<SelectOptionPB>, CustomCardData> hook, CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
) { ) {
selectOptionHook(cellData, cardData) { final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
if (cellData is List<SelectOptionPB>) { renderHook[FieldType.SingleSelect] = hookFn;
hook(cellData, cardData); renderHook[FieldType.MultiSelect] = hookFn;
}
/// Add a render hook for the [FieldType.RichText]
void addTextCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Number]
void addNumberCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.Number] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Date]
void addDateCellHook(
CellRenderHook<DateCellDataPB, CustomCardData?> hook,
) {
renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
}
CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
CellRenderHook<C, CustomCardData?> hook,
) {
hookFn(cellData, cardData, buildContext) {
if (cellData == null) {
return null;
}
if (cellData is C) {
return hook(cellData, cardData, buildContext);
} else {
Log.debug("Unexpected cellData type: ${cellData.runtimeType}");
return null;
} }
} }
renderHook[FieldType.SingleSelect] = selectOptionHook; return hookFn;
renderHook[FieldType.MultiSelect] = selectOptionHook;
} }
} }

View File

@ -44,6 +44,8 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
: svgWidget('editor/editor_uncheck'); : svgWidget('editor/editor_uncheck');
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: FlowyIconButton( child: FlowyIconButton(
iconPadding: EdgeInsets.zero, iconPadding: EdgeInsets.zero,
icon: icon, icon: icon,
@ -52,6 +54,7 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
.read<CheckboxCardCellBloc>() .read<CheckboxCardCellBloc>()
.add(const CheckboxCardCellEvent.select()), .add(const CheckboxCardCellEvent.select()),
), ),
),
); );
}, },
), ),

View File

@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart';
import '../define.dart'; import '../define.dart';
import 'card_cell.dart'; import 'card_cell.dart';
class DateCardCell extends CardCell { class DateCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const DateCardCell({ const DateCardCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
this.renderHook,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -42,6 +44,15 @@ class _DateCardCellState extends State<DateCardCell> {
if (state.dateStr.isEmpty) { if (state.dateStr.isEmpty) {
return const SizedBox(); return const SizedBox();
} else { } else {
Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(

View File

@ -7,13 +7,24 @@ import '../bloc/number_card_cell_bloc.dart';
import '../define.dart'; import '../define.dart';
import 'card_cell.dart'; import 'card_cell.dart';
class NumberCardCell extends CardCell { class NumberCardCellStyle extends CardCellStyle {
final double fontSize;
NumberCardCellStyle(this.fontSize);
}
class NumberCardCell<CustomCardData>
extends CardCell<CustomCardData, NumberCardCellStyle> {
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
const NumberCardCell({ const NumberCardCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
CustomCardData? cardData,
NumberCardCellStyle? style,
this.renderHook,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key, style: style, cardData: cardData);
@override @override
State<NumberCardCell> createState() => _NumberCardCellState(); State<NumberCardCell> createState() => _NumberCardCellState();
@ -42,6 +53,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
if (state.content.isEmpty) { if (state.content.isEmpty) {
return const SizedBox(); return const SizedBox();
} else { } else {
Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
@ -50,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
), ),
child: FlowyText.medium( child: FlowyText.medium(
state.content, state.content,
fontSize: 14, fontSize: widget.style?.fontSize ?? 14,
), ),
), ),
); );

View File

@ -11,17 +11,18 @@ import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {} class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle> class SelectOptionCardCell<CustomCardData>
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
with EditableCell { with EditableCell {
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, T>? renderHook; final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
@override @override
final EditableCardNotifier? editableNotifier; final EditableCardNotifier? editableNotifier;
SelectOptionCardCell({ SelectOptionCardCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
required T? cardData, required CustomCardData? cardData,
this.renderHook, this.renderHook,
this.editableNotifier, this.editableNotifier,
Key? key, Key? key,
@ -57,6 +58,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
Widget? custom = widget.renderHook?.call( Widget? custom = widget.renderHook?.call(
state.selectedOptions, state.selectedOptions,
widget.cardData, widget.cardData,
context,
); );
if (custom != null) { if (custom != null) {
return custom; return custom;

View File

@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle {
TextCardCellStyle(this.fontSize); TextCardCellStyle(this.fontSize);
} }
class TextCardCell extends CardCell<String, TextCardCellStyle> class TextCardCell<CustomCardData>
with EditableCell { extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
@override @override
final EditableCardNotifier? editableNotifier; final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<String, CustomCardData>? renderHook;
const TextCardCell({ const TextCardCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
required CustomCardData? cardData,
this.editableNotifier, this.editableNotifier,
this.renderHook,
TextCardCellStyle? style, TextCardCellStyle? style,
Key? key, Key? key,
}) : super(key: key, style: style); }) : super(key: key, style: style, cardData: cardData);
@override @override
State<TextCardCell> createState() => _TextCardCellState(); State<TextCardCell> createState() => _TextCardCellState();
@ -104,6 +107,16 @@ class _TextCardCellState extends State<TextCardCell> {
return previous != current; return previous != current;
}, },
builder: (context, state) { builder: (context, state) {
// Returns a custom render widget
Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
if (state.content.isEmpty && if (state.content.isEmpty &&
state.enableEdit == false && state.enableEdit == false &&
focusWhenInit == false) { focusWhenInit == false) {

View File

@ -8,13 +8,21 @@ import '../bloc/url_card_cell_bloc.dart';
import '../define.dart'; import '../define.dart';
import 'card_cell.dart'; import 'card_cell.dart';
class URLCardCell extends CardCell { class URLCardCellStyle extends CardCellStyle {
final double fontSize;
URLCardCellStyle(this.fontSize);
}
class URLCardCell<CustomCardData>
extends CardCell<CustomCardData, URLCardCellStyle> {
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
const URLCardCell({ const URLCardCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
URLCardCellStyle? style,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key, style: style);
@override @override
State<URLCardCell> createState() => _URLCardCellState(); State<URLCardCell> createState() => _URLCardCellState();
@ -55,7 +63,7 @@ class _URLCardCellState extends State<URLCardCell> {
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyMedium! .bodyMedium!
.size(FontSizes.s14) .size(widget.style?.fontSize ?? FontSizes.s14)
.textColor(Theme.of(context).colorScheme.primary) .textColor(Theme.of(context).colorScheme.primary)
.underline, .underline,
), ),

View File

@ -4,13 +4,13 @@ import 'package:styled_widget/styled_widget.dart';
import 'accessory.dart'; import 'accessory.dart';
class BoardCardContainer extends StatelessWidget { class RowCardContainer extends StatelessWidget {
final Widget child; final Widget child;
final CardAccessoryBuilder? accessoryBuilder; final CardAccessoryBuilder? accessoryBuilder;
final bool Function()? buildAccessoryWhen; final bool Function()? buildAccessoryWhen;
final void Function(BuildContext) openCard; final void Function(BuildContext) openCard;
final void Function(AccessoryType) openAccessory; final void Function(AccessoryType) openAccessory;
const BoardCardContainer({ const RowCardContainer({
required this.child, required this.child,
required this.openCard, required this.openCard,
required this.openAccessory, required this.openAccessory,

View File

@ -20,7 +20,7 @@ class ChecklistProgressBar extends StatelessWidget {
percent: percent, percent: percent,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
progressColor: Theme.of(context).colorScheme.primary, progressColor: Theme.of(context).colorScheme.primary,
backgroundColor: AFThemeExtension.of(context).progressBarBGcolor, backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(5), barRadius: const Radius.circular(5),
); );
} }

View File

@ -289,10 +289,7 @@ Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
Option<DateCellData> dateData = none(); Option<DateCellData> dateData = none();
if (cellData != null) { if (cellData != null) {
final timestamp = cellData.timestamp * 1000; final timestamp = cellData.timestamp * 1000;
final date = DateTime.fromMillisecondsSinceEpoch( final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
timestamp.toInt(),
isUtc: true,
);
dateData = Some( dateData = Some(
DateCellData( DateCellData(
date: date, date: date,

View File

@ -1,11 +1,11 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
part 'number_cell_bloc.freezed.dart'; part 'number_cell_bloc.freezed.dart';
//
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> { class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
final NumberCellController cellController; final NumberCellController cellController;
void Function()? _onCellChangedFn; void Function()? _onCellChangedFn;
@ -22,17 +22,18 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
didReceiveCellUpdate: (cellContent) { didReceiveCellUpdate: (cellContent) {
emit(state.copyWith(cellContent: cellContent ?? "")); emit(state.copyWith(cellContent: cellContent ?? ""));
}, },
updateCell: (text) { updateCell: (text) async {
if (state.cellContent != text) { if (state.cellContent != text) {
emit(state.copyWith(cellContent: text)); emit(state.copyWith(cellContent: text));
cellController.saveCellData( await cellController.saveCellData(text);
text,
onFinish: (result) { // If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
result.fold( // So for every cell data that will be formatted in the backend.
() {}, // It needs to get the formatted data after saving.
(err) => Log.error(err), add(
); NumberCellEvent.didReceiveCellUpdate(
}, cellController.getCellData(),
),
); );
} }
}, },

View File

@ -8,9 +8,13 @@ import '../../cell_builder.dart';
class GridTextCellStyle extends GridCellStyle { class GridTextCellStyle extends GridCellStyle {
String? placeholder; String? placeholder;
TextStyle? textStyle;
bool? autofocus;
GridTextCellStyle({ GridTextCellStyle({
this.placeholder, this.placeholder,
this.textStyle,
this.autofocus,
}); });
} }
@ -66,7 +70,9 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
controller: _controller, controller: _controller,
focusNode: focusNode, focusNode: focusNode,
maxLines: null, maxLines: null,
style: Theme.of(context).textTheme.bodyMedium, style: widget.cellStyle?.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle?.autofocus ?? false,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.only( contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top, top: GridSize.cellContentInsets.top,

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -43,83 +44,85 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
} }
class _RowDetailPageState extends State<RowDetailPage> { class _RowDetailPageState extends State<RowDetailPage> {
final padding = const EdgeInsets.symmetric(
horizontal: 40,
vertical: 20,
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyDialog( return FlowyDialog(
child: BlocProvider( child: BlocProvider(
create: (context) { create: (context) {
final bloc = RowDetailBloc( return RowDetailBloc(dataController: widget.dataController)
dataController: widget.dataController, ..add(const RowDetailEvent.initial());
);
bloc.add(const RowDetailEvent.initial());
return bloc;
}, },
child: Padding( child: ListView(
padding: padding,
child: Column(
children: [ children: [
const _Header(), // using ListView here for future expansion:
Expanded( // - header and cover image
child: _PropertyColumn( // - lower rich text area
cellBuilder: widget.cellBuilder, IntrinsicHeight(child: _responsiveRowInfo()),
viewId: widget.dataController.viewId, const Divider(height: 1.0),
), const SizedBox(height: 10),
),
], ],
), ),
), ),
),
); );
} }
}
class _Header extends StatelessWidget { Widget _responsiveRowInfo() {
const _Header({Key? key}) : super(key: key); final rowDataColumn = _PropertyColumn(
cellBuilder: widget.cellBuilder,
@override viewId: widget.dataController.viewId,
Widget build(BuildContext context) { );
return SizedBox( final rowOptionColumn = _RowOptionColumn(
height: 30, viewId: widget.dataController.viewId,
child: Row( rowId: widget.dataController.rowId,
children: const [Spacer(), _CloseButton()], );
if (MediaQuery.of(context).size.width > 800) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 4,
child: Padding(
padding: const EdgeInsets.fromLTRB(50, 50, 20, 20),
child: rowDataColumn,
), ),
),
const VerticalDivider(width: 1.0),
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
child: rowOptionColumn,
),
),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
child: rowDataColumn,
),
const Divider(height: 1.0),
Padding(
padding: const EdgeInsets.all(20),
child: rowOptionColumn,
)
],
); );
} }
}
class _CloseButton extends StatelessWidget {
const _CloseButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 24,
onPressed: () => FlowyOverlay.pop(context),
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
icon: svgWidget(
"home/close",
color: Theme.of(context).iconTheme.color,
),
);
} }
} }
class _PropertyColumn extends StatelessWidget { class _PropertyColumn extends StatelessWidget {
final String viewId; final String viewId;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
final ScrollController _scrollController; const _PropertyColumn({
_PropertyColumn({
required this.viewId, required this.viewId,
required this.cellBuilder, required this.cellBuilder,
Key? key, Key? key,
}) : _scrollController = ScrollController(), }) : super(key: key);
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -127,63 +130,61 @@ class _PropertyColumn extends StatelessWidget {
buildWhen: (previous, current) => previous.gridCells != current.gridCells, buildWhen: (previous, current) => previous.gridCells != current.gridCells,
builder: (context, state) { builder: (context, state) {
return Column( return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: _wrapScrollbar(buildPropertyCells(state))), _RowTitle(
const VSpace(10), cellId: state.gridCells
_CreatePropertyButton( .firstWhereOrNull((e) => e.fieldInfo.isPrimary),
viewId: viewId, cellBuilder: cellBuilder,
onClosed: _scrollToNewProperty,
), ),
const VSpace(20),
...state.gridCells
.where((element) => !element.fieldInfo.isPrimary)
.map(
(cell) => Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: _PropertyCell(
cellId: cell,
cellBuilder: cellBuilder,
),
),
)
.toList(),
const VSpace(20),
_CreatePropertyButton(viewId: viewId),
], ],
); );
}, },
); );
} }
}
Widget buildPropertyCells(RowDetailState state) { class _RowTitle extends StatelessWidget {
return ListView.separated( final CellIdentifier? cellId;
controller: _scrollController, final GridCellBuilder cellBuilder;
itemCount: state.gridCells.length, const _RowTitle({this.cellId, required this.cellBuilder, Key? key})
itemBuilder: (BuildContext context, int index) { : super(key: key);
return _PropertyCell(
cellId: state.gridCells[index], @override
cellBuilder: cellBuilder, Widget build(BuildContext context) {
); if (cellId == null) {
}, return const SizedBox();
separatorBuilder: (BuildContext context, int index) {
return const VSpace(2);
},
);
} }
final style = GridTextCellStyle(
Widget _wrapScrollbar(Widget child) { placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
return ScrollbarListStack( textStyle: Theme.of(context).textTheme.titleLarge,
axis: Axis.vertical, autofocus: true,
controller: _scrollController,
barSize: GridSize.scrollBarSize,
autoHideScrollbar: false,
child: child,
); );
} return cellBuilder.build(cellId!, style: style);
void _scrollToNewProperty() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
});
} }
} }
class _CreatePropertyButton extends StatefulWidget { class _CreatePropertyButton extends StatefulWidget {
final String viewId; final String viewId;
final VoidCallback onClosed;
const _CreatePropertyButton({ const _CreatePropertyButton({
required this.viewId, required this.viewId,
required this.onClosed,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -206,10 +207,9 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
constraints: BoxConstraints.loose(const Size(240, 200)), constraints: BoxConstraints.loose(const Size(240, 200)),
controller: popoverController, controller: popoverController,
direction: PopoverDirection.topWithLeftAligned, direction: PopoverDirection.topWithLeftAligned,
onClose: widget.onClosed, margin: EdgeInsets.zero,
child: Container( child: SizedBox(
height: 40, height: 40,
decoration: _makeBoxDecoration(context),
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium( text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(), LocaleKeys.grid_field_newProperty.tr(),
@ -243,14 +243,6 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
}, },
); );
} }
BoxDecoration _makeBoxDecoration(BuildContext context) {
final borderSide =
BorderSide(color: Theme.of(context).dividerColor, width: 1.0);
return BoxDecoration(
border: Border(top: borderSide),
);
}
} }
class _PropertyCell extends StatefulWidget { class _PropertyCell extends StatefulWidget {
@ -376,3 +368,69 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
} }
throw UnimplementedError; throw UnimplementedError;
} }
class _RowOptionColumn extends StatelessWidget {
final String rowId;
const _RowOptionColumn({
required String viewId,
required this.rowId,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: FlowyText(LocaleKeys.grid_row_action.tr()),
),
const VSpace(15),
_DeleteButton(rowId: rowId),
_DuplicateButton(rowId: rowId),
],
);
}
}
class _DeleteButton extends StatelessWidget {
final String rowId;
const _DeleteButton({required this.rowId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
leftIcon: const FlowySvg(name: "home/trash"),
onTap: () {
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
FlowyOverlay.pop(context);
},
),
);
}
}
class _DuplicateButton extends StatelessWidget {
final String rowId;
const _DuplicateButton({required this.rowId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
leftIcon: const FlowySvg(name: "grid/duplicate"),
onTap: () {
context.read<RowDetailBloc>().add(RowDetailEvent.duplicateRow(rowId));
FlowyOverlay.pop(context);
},
),
);
}
}

View File

@ -1,15 +1,6 @@
import 'package:appflowy/plugins/document/presentation/plugins/board/board_view_menu_item.dart'; import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart';
import 'package:dartz/dartz.dart' as dartz; import 'package:dartz/dartz.dart' as dartz;
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -20,8 +11,6 @@ import '../../startup/startup.dart';
import 'application/doc_bloc.dart'; import 'application/doc_bloc.dart';
import 'editor_styles.dart'; import 'editor_styles.dart';
import 'presentation/banner.dart'; import 'presentation/banner.dart';
import 'presentation/plugins/grid/grid_view_menu_item.dart';
import 'presentation/plugins/board/board_menu_item.dart';
class DocumentPage extends StatefulWidget { class DocumentPage extends StatefulWidget {
final VoidCallback onDeleted; final VoidCallback onDeleted;

View File

@ -1,32 +1,63 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
EditorStyle customEditorTheme(BuildContext context) { EditorStyle customEditorTheme(BuildContext context) {
final documentStyle = context.watch<DocumentAppearanceCubit>().state; final documentStyle = context.watch<DocumentAppearanceCubit>().state;
var editorStyle = Theme.of(context).brightness == Brightness.dark final theme = Theme.of(context);
? EditorStyle.dark
: EditorStyle.light; var editorStyle = EditorStyle(
editorStyle = editorStyle.copyWith( // Editor styles
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 0), padding: const EdgeInsets.symmetric(horizontal: 100),
textStyle: editorStyle.textStyle?.copyWith( backgroundColor: theme.colorScheme.surface,
cursorColor: theme.colorScheme.primary,
// Text styles
textPadding: const EdgeInsets.symmetric(vertical: 8.0),
textStyle: TextStyle(
fontFamily: 'poppins', fontFamily: 'poppins',
fontSize: documentStyle.fontSize, fontSize: documentStyle.fontSize,
color: theme.colorScheme.onBackground,
), ),
placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith( selectionColor: theme.colorScheme.tertiary.withOpacity(0.2),
fontFamily: 'poppins', // Selection menu
fontSize: documentStyle.fontSize, selectionMenuBackgroundColor: theme.cardColor,
), selectionMenuItemTextColor: theme.iconTheme.color,
bold: editorStyle.bold?.copyWith( selectionMenuItemIconColor: theme.colorScheme.onBackground,
fontWeight: FontWeight.w600, selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,
selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,
selectionMenuItemSelectedColor: theme.hoverColor,
// Toolbar and its item's style
toolbarColor: theme.colorScheme.onTertiary,
toolbarElevation: 0,
lineHeight: 1.5,
placeholderTextStyle:
TextStyle(fontSize: documentStyle.fontSize, color: theme.hintColor),
bold: const TextStyle(
fontFamily: 'poppins-Bold', fontFamily: 'poppins-Bold',
fontWeight: FontWeight.w600,
), ),
backgroundColor: Theme.of(context).colorScheme.surface, italic: const TextStyle(fontStyle: FontStyle.italic),
selectionMenuBackgroundColor: Theme.of(context).cardColor, underline: const TextStyle(decoration: TextDecoration.underline),
selectionMenuItemSelectedIconColor: Theme.of(context).colorScheme.onSurface, strikethrough: const TextStyle(decoration: TextDecoration.lineThrough),
selectionMenuItemSelectedTextColor: Theme.of(context).colorScheme.onSurface, href: TextStyle(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
highlightColorHex: '0x6000BCF0',
code: GoogleFonts.robotoMono(
textStyle: TextStyle(
fontSize: documentStyle.fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
backgroundColor: theme.colorScheme.inverseSurface,
),
),
popupMenuFGColor: theme.iconTheme.color,
popupMenuHoverColor: theme.colorScheme.tertiaryContainer,
); );
return editorStyle; return editorStyle;
} }

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.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.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
@ -24,6 +25,8 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor;
final foregroundColor = Theme.of(context).colorScheme.onBackground;
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>( return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (context, state) { builder: (context, state) {
return Column( return Column(
@ -43,10 +46,16 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
onPressed: (int index) { onPressed: (int index) {
_updateSelectedFontSize(_fontSizes[index].item2); _updateSelectedFontSize(_fontSizes[index].item2);
}, },
color: foregroundColor,
borderRadius: const BorderRadius.all(Radius.circular(5)), borderRadius: const BorderRadius.all(Radius.circular(5)),
selectedColor: Theme.of(context).colorScheme.tertiary, borderColor: foregroundColor,
fillColor: Theme.of(context).colorScheme.primary, borderWidth: 0.5,
color: Theme.of(context).hintColor, // when selected
selectedColor: foregroundColor,
selectedBorderColor: foregroundColor,
fillColor: selectedBgColor,
// when hover
hoverColor: selectedBgColor.withOpacity(0.3),
constraints: const BoxConstraints( constraints: const BoxConstraints(
minHeight: 40.0, minHeight: 40.0,
minWidth: 80.0, minWidth: 80.0,

View File

@ -12,6 +12,7 @@ class DocumentMoreButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopupMenuButton<int>( return PopupMenuButton<int>(
color: Theme.of(context).colorScheme.surfaceVariant,
offset: const Offset(0, 30), offset: const Offset(0, 30),
itemBuilder: (context) { itemBuilder: (context) {
return [ return [

View File

@ -1,6 +1,5 @@
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/src/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy_editor_plugins/src/extensions/theme_extension.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -192,10 +191,12 @@ class _CalloutWidgetState extends State<_CalloutWidget> with SelectableMixin {
Widget _buildColorPicker() { Widget _buildColorPicker() {
return FlowyColorPicker( return FlowyColorPicker(
colors: FlowyTint.values colors: FlowyTint.values
.map((t) => ColorOption( .map(
(t) => ColorOption(
color: t.color(context), color: t.color(context),
name: t.tintName(AppFlowyEditorLocalizations.current), name: t.tintName(AppFlowyEditorLocalizations.current),
)) ),
)
.toList(), .toList(),
selected: tint.color(context), selected: tint.color(context),
onTap: (color, index) { onTap: (color, index) {

View File

@ -157,11 +157,11 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
? TextSpan(text: node.value) ? TextSpan(text: node.value)
: TextSpan( : TextSpan(
text: node.value, text: node.value,
style: _builtInCodeBlockTheme[node.className!])); style: _builtInCodeBlockTheme[node.className!],),);
} else if (node.children != null) { } else if (node.children != null) {
List<TextSpan> tmp = []; List<TextSpan> tmp = [];
currentSpans.add(TextSpan( currentSpans.add(TextSpan(
children: tmp, style: _builtInCodeBlockTheme[node.className!])); children: tmp, style: _builtInCodeBlockTheme[node.className!],),);
stack.add(currentSpans); stack.add(currentSpans);
currentSpans = tmp; currentSpans = tmp;
@ -213,7 +213,7 @@ const _builtInCodeBlockTheme = {
'attr': TextStyle(color: Color(0xff836C28)), 'attr': TextStyle(color: Color(0xff836C28)),
'subst': TextStyle(color: Color(0xff000000)), 'subst': TextStyle(color: Color(0xff000000)),
'formula': TextStyle( 'formula': TextStyle(
backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic), backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic,),
'addition': TextStyle(backgroundColor: Color(0xffbaeeba)), 'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)), 'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
'selector-id': TextStyle(color: Color(0xff9b703f)), 'selector-id': TextStyle(color: Color(0xff9b703f)),

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/src/code_block/code_block_node_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';

View File

@ -2,11 +2,8 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover_bloc.dart'; import 'package:appflowy/plugins/document/presentation/plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy_editor/appflowy_editor.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
@ -257,8 +254,6 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
if (index == 0) { if (index == 0) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.15),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
@ -270,6 +265,8 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
Icons.add, Icons.add,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
hoverColor:
Theme.of(context).colorScheme.primary.withOpacity(0.15),
width: 20, width: 20,
onPressed: () { onPressed: () {
setState(() { setState(() {

View File

@ -145,7 +145,7 @@ class _NetworkImageUrlInputState extends State<NetworkImageUrlInput> {
}, },
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
fillColor: buttonDisabled fillColor: buttonDisabled
? Colors.grey ? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
height: 36, height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(), title: LocaleKeys.document_plugins_cover_add.tr(),
@ -174,7 +174,7 @@ class ImagePickerActionButtons extends StatelessWidget {
children: [ children: [
FlowyTextButton( FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(), LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent, hoverColor: Theme.of(context).colorScheme.secondaryContainer,
fillColor: Colors.transparent, fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => onBackPressed(), onPressed: () => onBackPressed(),
@ -182,7 +182,7 @@ class ImagePickerActionButtons extends StatelessWidget {
FlowyTextButton( FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(), LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () => onSave(), onPressed: () => onSave(),
hoverColor: Colors.transparent, hoverColor: Theme.of(context).colorScheme.secondaryContainer,
fillColor: Colors.transparent, fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary, fontColor: Theme.of(context).colorScheme.primary,
@ -204,15 +204,26 @@ class CoverImagePreviewWidget extends StatefulWidget {
class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> { class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
_buildFilePickerWidget(BuildContext ctx) { _buildFilePickerWidget(BuildContext ctx) {
return Column( return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: Corners.s6Border,
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1,
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
svgWidget( const FlowySvg(
"editor/add", name: 'editor/add',
size: const Size(20, 20), size: Size(20, 20),
), ),
const SizedBox( const SizedBox(
width: 3, width: 3,
@ -227,25 +238,27 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
), ),
FlowyText( FlowyText(
LocaleKeys.document_plugins_cover_or.tr(), LocaleKeys.document_plugins_cover_or.tr(),
color: Colors.grey, fontWeight: FontWeight.w300,
), ),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
FlowyButton( FlowyButton(
hoverColor: Theme.of(context).hoverColor,
onTap: () { onTap: () {
ctx.read<CoverImagePickerBloc>().add(const PickFileImage()); ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
}, },
useIntrinsicWidth: true, useIntrinsicWidth: true,
leftIcon: svgWidget( leftIcon: const FlowySvg(
"file_icon", name: 'file_icon',
size: const Size(25, 25), size: Size(20, 20),
), ),
text: FlowyText( text: FlowyText(
LocaleKeys.document_plugins_cover_pickFromFiles.tr(), LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
), ),
), ),
], ],
),
); );
} }

View File

@ -5,7 +5,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cove
import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -393,22 +393,33 @@ class _CoverImageState extends State<_CoverImage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AppFlowyPopover( AppFlowyPopover(
onClose: () {
setOverlayButtonsHidden(true);
},
offset: const Offset(-125, 10), offset: const Offset(-125, 10),
controller: popoverController, controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(380, 450)), constraints: BoxConstraints.loose(const Size(380, 450)),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Visibility(
maintainState: true,
maintainAnimation: true,
maintainSize: true,
visible: !isOverlayButtonsHidden,
child: RoundedTextButton( child: RoundedTextButton(
onPressed: () { onPressed: () {
popoverController.show(); popoverController.show();
setOverlayButtonsHidden(true);
}, },
hoverColor: Theme.of(context).colorScheme.surface, hoverColor: Theme.of(context).colorScheme.surface,
textColor: Theme.of(context).colorScheme.tertiary, textColor: Theme.of(context).colorScheme.tertiary,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8), fillColor:
Theme.of(context).colorScheme.surface.withOpacity(0.5),
width: 120, width: 120,
height: 28, height: 28,
title: LocaleKeys.document_plugins_cover_changeCover.tr(), title: LocaleKeys.document_plugins_cover_changeCover.tr(),
), ),
),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return ChangeCoverPopover( return ChangeCoverPopover(
node: widget.node, node: widget.node,
@ -418,9 +429,14 @@ class _CoverImageState extends State<_CoverImage> {
}, },
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
FlowyIconButton( Visibility(
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8), maintainAnimation: true,
maintainSize: true,
maintainState: true,
visible: !isOverlayButtonsHidden,
child: FlowyIconButton(
hoverColor: Theme.of(context).colorScheme.surface, hoverColor: Theme.of(context).colorScheme.surface,
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
iconPadding: const EdgeInsets.all(5), iconPadding: const EdgeInsets.all(5),
width: 28, width: 28,
icon: svgWidget( icon: svgWidget(
@ -431,6 +447,7 @@ class _CoverImageState extends State<_CoverImage> {
widget.onCoverChanged(CoverSelectionType.initial, null); widget.onCoverChanged(CoverSelectionType.initial, null);
}, },
), ),
),
], ],
), ),
); );
@ -477,7 +494,14 @@ class _CoverImageState extends State<_CoverImage> {
break; break;
} }
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error //OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
return SizedBox( return MouseRegion(
onEnter: (event) {
setOverlayButtonsHidden(false);
},
onExit: (event) {
setOverlayButtonsHidden(true);
},
child: SizedBox(
height: height, height: height,
child: OverflowBox( child: OverflowBox(
maxWidth: screenSize.width, maxWidth: screenSize.width,
@ -489,10 +513,13 @@ class _CoverImageState extends State<_CoverImage> {
width: double.infinity, width: double.infinity,
child: coverImage, child: coverImage,
), ),
hasCover ? _buildCoverOverlayButtons(context) : const SizedBox() hasCover
? _buildCoverOverlayButtons(context)
: const SizedBox.shrink()
], ],
), ),
), ),
),
); );
} }

View File

@ -4,7 +4,6 @@ import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart'; import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.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.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/plugins/divider/divider_node_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/src/divider/divider_node_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// insert divider into a document by typing three minuses. // insert divider into a document by typing three minuses.

View File

@ -48,7 +48,7 @@ void _showEmojiSelectionMenu(
), ),
), ),
); );
}); },);
Overlay.of(context).insert(_emojiSelectionMenu!); Overlay.of(context).insert(_emojiSelectionMenu!);

View File

@ -27,7 +27,7 @@ class Config {
const TextStyle(fontSize: 20, color: Colors.black26), const TextStyle(fontSize: 20, color: Colors.black26),
this.tabIndicatorAnimDuration = kTabScrollDuration, this.tabIndicatorAnimDuration = kTabScrollDuration,
this.categoryIcons = const CategoryIcons(), this.categoryIcons = const CategoryIcons(),
this.buttonMode = ButtonMode.MATERIAL}); this.buttonMode = ButtonMode.MATERIAL,});
/// Number of emojis per row /// Number of emojis per row
final int columns; final int columns;

View File

@ -27,14 +27,14 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
@override @override
void initState() { void initState() {
var initCategory = widget.state.categoryEmoji.indexWhere( var initCategory = widget.state.categoryEmoji.indexWhere(
(element) => element.category == widget.config.initCategory); (element) => element.category == widget.config.initCategory,);
if (initCategory == -1) { if (initCategory == -1) {
initCategory = 0; initCategory = 0;
} }
_tabController = TabController( _tabController = TabController(
initialIndex: initCategory, initialIndex: initCategory,
length: widget.state.categoryEmoji.length, length: widget.state.categoryEmoji.length,
vsync: this); vsync: this,);
_pageController = PageController(initialPage: initCategory); _pageController = PageController(initialPage: initCategory);
_emojiFocusNode.requestFocus(); _emojiFocusNode.requestFocus();
@ -79,7 +79,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
), ),
onPressed: () { onPressed: () {
widget.state.onBackspacePressed!(); widget.state.onBackspacePressed!();
}), },),
); );
} }
return Container(); return Container();
@ -161,7 +161,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
.asMap() .asMap()
.entries .entries
.map<Widget>((item) => _buildCategory( .map<Widget>((item) => _buildCategory(
item.value.category, emojiSize)) item.value.category, emojiSize,),)
.toList(), .toList(),
), ),
), ),
@ -207,7 +207,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
} }
Widget _buildButtonWidget( Widget _buildButtonWidget(
{required VoidCallback onPressed, required Widget child}) { {required VoidCallback onPressed, required Widget child,}) {
if (widget.config.buttonMode == ButtonMode.MATERIAL) { if (widget.config.buttonMode == ButtonMode.MATERIAL) {
return InkWell( return InkWell(
onTap: onPressed, onTap: onPressed,
@ -270,7 +270,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
widget.state.onEmojiSelected(categoryEmoji.category, emoji); widget.state.onEmojiSelected(categoryEmoji.category, emoji);
}, },
child: FittedBox( child: FittedBox(
fit: BoxFit.fill, fit: BoxFit.scaleDown,
child: Text( child: Text(
emoji.emoji, emoji.emoji,
textScaleFactor: 1.0, textScaleFactor: 1.0,
@ -279,7 +279,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
), ),
), ),
)); ),);
} }
Widget _buildNoRecent() { Widget _buildNoRecent() {
@ -288,6 +288,6 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
widget.config.noRecentsText, widget.config.noRecentsText,
style: widget.config.noRecentsStyle, style: widget.config.noRecentsStyle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)); ),);
} }
} }

View File

@ -191,29 +191,29 @@ class EmojiPickerState extends State<EmojiPicker> {
} }
categoryEmoji.addAll([ categoryEmoji.addAll([
CategoryEmoji(Category.SMILEYS, CategoryEmoji(Category.SMILEYS,
await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')), await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),),
CategoryEmoji(Category.ANIMALS, CategoryEmoji(Category.ANIMALS,
await _getAvailableEmojis(emoji_list.animals, title: 'animals')), await _getAvailableEmojis(emoji_list.animals, title: 'animals'),),
CategoryEmoji(Category.FOODS, CategoryEmoji(Category.FOODS,
await _getAvailableEmojis(emoji_list.foods, title: 'foods')), await _getAvailableEmojis(emoji_list.foods, title: 'foods'),),
CategoryEmoji( CategoryEmoji(
Category.ACTIVITIES, Category.ACTIVITIES,
await _getAvailableEmojis(emoji_list.activities, await _getAvailableEmojis(emoji_list.activities,
title: 'activities')), title: 'activities',),),
CategoryEmoji(Category.TRAVEL, CategoryEmoji(Category.TRAVEL,
await _getAvailableEmojis(emoji_list.travel, title: 'travel')), await _getAvailableEmojis(emoji_list.travel, title: 'travel'),),
CategoryEmoji(Category.OBJECTS, CategoryEmoji(Category.OBJECTS,
await _getAvailableEmojis(emoji_list.objects, title: 'objects')), await _getAvailableEmojis(emoji_list.objects, title: 'objects'),),
CategoryEmoji(Category.SYMBOLS, CategoryEmoji(Category.SYMBOLS,
await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')), await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),),
CategoryEmoji(Category.FLAGS, CategoryEmoji(Category.FLAGS,
await _getAvailableEmojis(emoji_list.flags, title: 'flags')) await _getAvailableEmojis(emoji_list.flags, title: 'flags'),)
]); ]);
} }
// Get available emoji for given category title // Get available emoji for given category title
Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map, Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map,
{required String title}) async { {required String title,}) async {
Map<String, String>? newMap; Map<String, String>? newMap;
// Get Emojis cached locally if available // Get Emojis cached locally if available
@ -236,7 +236,7 @@ class EmojiPickerState extends State<EmojiPicker> {
// Check if emoji is available on current platform // Check if emoji is available on current platform
Future<Map<String, String>?> _getPlatformAvailableEmoji( Future<Map<String, String>?> _getPlatformAvailableEmoji(
Map<String, String> emoji) async { Map<String, String> emoji,) async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
Map<String, String>? filtered = {}; Map<String, String>? filtered = {};
var delimiter = '|'; var delimiter = '|';
@ -244,7 +244,7 @@ class EmojiPickerState extends State<EmojiPicker> {
var entries = emoji.values.join(delimiter); var entries = emoji.values.join(delimiter);
var keys = emoji.keys.join(delimiter); var keys = emoji.keys.join(delimiter);
var result = (await platform.invokeMethod<String>('checkAvailability', var result = (await platform.invokeMethod<String>('checkAvailability',
{'emojiKeys': keys, 'emojiEntries': entries})) as String; {'emojiKeys': keys, 'emojiEntries': entries},)) as String;
var resultKeys = result.split(delimiter); var resultKeys = result.split(delimiter);
for (var i = 0; i < resultKeys.length; i++) { for (var i = 0; i < resultKeys.length; i++) {
filtered[resultKeys[i]] = emoji[resultKeys[i]]!; filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
@ -272,7 +272,7 @@ class EmojiPickerState extends State<EmojiPicker> {
// Stores filtered emoji locally for faster access next time // Stores filtered emoji locally for faster access next time
Future<void> _cacheFilteredEmojis( Future<void> _cacheFilteredEmojis(
String title, Map<String, String> emojis) async { String title, Map<String, String> emojis,) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
var emojiJson = jsonEncode(emojis); var emojiJson = jsonEncode(emojis);
prefs.setString(title, emojiJson); prefs.setString(title, emojiJson);
@ -305,7 +305,7 @@ class EmojiPickerState extends State<EmojiPicker> {
recentEmoji.sort((a, b) => b.counter - a.counter); recentEmoji.sort((a, b) => b.counter - a.counter);
// Limit entries to recentsLimit // Limit entries to recentsLimit
recentEmoji = recentEmoji.sublist( recentEmoji = recentEmoji.sublist(
0, min(widget.config.recentsLimit, recentEmoji.length)); 0, min(widget.config.recentsLimit, recentEmoji.length),);
// save locally // save locally
prefs.setString('recent', jsonEncode(recentEmoji)); prefs.setString('recent', jsonEncode(recentEmoji));
} }

View File

@ -1,3 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@ -1,168 +0,0 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
ShortcutEvent insertHorizontalRule = ShortcutEvent(
key: 'Horizontal rule',
command: 'Minus',
handler: _insertHorzaontalRule,
);
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
final selection = editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (textNodes.length != 1 || selection == null) {
return KeyEventResult.ignored;
}
final textNode = textNodes.first;
if (textNode.toPlainText() == '--') {
final transaction = editorState.transaction
..deleteText(textNode, 0, 2)
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0);
editorState.apply(transaction);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
name: 'Horizontal rule',
icon: (editorState, onSelected) => Icon(
Icons.horizontal_rule,
color: onSelected
? editorState.editorStyle.selectionMenuItemSelectedIconColor
: editorState.editorStyle.selectionMenuItemIconColor,
size: 18.0,
),
keywords: ['horizontal rule'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || textNodes.isEmpty) {
return;
}
final textNode = textNodes.first;
if (textNode.toPlainText().isEmpty) {
final transaction = editorState.transaction
..insertNode(
textNode.path,
Node(
type: 'horizontal_rule',
children: LinkedList(),
attributes: {},
),
)
..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0);
editorState.apply(transaction);
} else {
final transaction = editorState.transaction
..insertNode(
selection.end.path.next,
TextNode(
children: LinkedList(),
attributes: {
'subtype': 'horizontal_rule',
},
delta: Delta()..insert('---'),
),
)
..afterSelection = selection;
editorState.apply(transaction);
}
},
);
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _HorizontalRuleWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return true;
};
}
class _HorizontalRuleWidget extends StatefulWidget {
const _HorizontalRuleWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
}
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
with SelectableMixin {
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Container(
height: 1,
color: Colors.grey,
),
);
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
bool get shouldCursorBlink => false;
@override
CursorStyle get cursorStyle => CursorStyle.borderLine;
@override
Rect? getCursorRectInPosition(Position position) {
final size = _renderBox.size;
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
}
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}

View File

@ -1,4 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_math_fork/flutter_math.dart'; import 'package:flutter_math_fork/flutter_math.dart';
@ -131,14 +136,14 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)), borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: _isHover || _mathEquation.isEmpty color: _isHover || _mathEquation.isEmpty
? Colors.grey[200] ? Theme.of(context).colorScheme.tertiaryContainer
: Colors.transparent, : Colors.transparent,
), ),
child: Center( child: Center(
child: _mathEquation.isEmpty child: _mathEquation.isEmpty
? Text( ? FlowyText.medium(
'Add a Math Equation', LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
style: widget.editorState.editorStyle.placeholderTextStyle, fontSize: 16,
) )
: Math.tex( : Math.tex(
_mathEquation, _mathEquation,
@ -155,7 +160,10 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
builder: (context) { builder: (context) {
final controller = TextEditingController(text: _mathEquation); final controller = TextEditingController(text: _mathEquation);
return AlertDialog( return AlertDialog(
title: const Text('Edit Math Equation'), backgroundColor: Theme.of(context).canvasColor,
title: Text(
LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),
),
content: RawKeyboardListener( content: RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
onKey: (key) { onKey: (key) {
@ -178,15 +186,17 @@ class _MathEquationNodeWidgetState extends State<_MathEquationNodeWidget> {
), ),
), ),
actions: [ actions: [
TextButton( SecondaryTextButton(
LocaleKeys.button_Cancel.tr(),
onPressed: () => _dismiss(context), onPressed: () => _dismiss(context),
child: const Text('Cancel'),
), ),
TextButton( PrimaryTextButton(
LocaleKeys.button_Done.tr(),
onPressed: () => _updateMathEquation(controller.text, context), onPressed: () => _updateMathEquation(controller.text, context),
child: const Text('Done'),
), ),
], ],
actionsPadding: const EdgeInsets.only(bottom: 20),
actionsAlignment: MainAxisAlignment.spaceAround,
); );
}, },
); );

View File

@ -50,6 +50,7 @@ abstract class OpenAIRepository {
String? suffix, String? suffix,
int maxTokens = 2048, int maxTokens = 2048,
double temperature = 0.3, double temperature = 0.3,
bool useAction = false,
}); });
/// Get edits from GPT-3 /// Get edits from GPT-3

View File

@ -5,7 +5,8 @@ import 'package:easy_localization/easy_localization.dart';
enum SmartEditAction { enum SmartEditAction {
summarize, summarize,
fixSpelling; fixSpelling,
improveWriting;
String get toInstruction { String get toInstruction {
switch (this) { switch (this) {
@ -13,6 +14,8 @@ enum SmartEditAction {
return 'Tl;dr'; return 'Tl;dr';
case SmartEditAction.fixSpelling: case SmartEditAction.fixSpelling:
return 'Correct this to standard English:'; return 'Correct this to standard English:';
case SmartEditAction.improveWriting:
return 'Rewrite this in your own words:';
} }
} }
@ -22,6 +25,8 @@ enum SmartEditAction {
return '$input\n\nTl;dr'; return '$input\n\nTl;dr';
case SmartEditAction.fixSpelling: case SmartEditAction.fixSpelling:
return 'Correct this to standard English:\n\n$input'; return 'Correct this to standard English:\n\n$input';
case SmartEditAction.improveWriting:
return 'Rewrite this:\n\n$input';
} }
} }
@ -31,6 +36,8 @@ enum SmartEditAction {
return SmartEditAction.summarize; return SmartEditAction.summarize;
case 1: case 1:
return SmartEditAction.fixSpelling; return SmartEditAction.fixSpelling;
case 2:
return SmartEditAction.improveWriting;
} }
return SmartEditAction.fixSpelling; return SmartEditAction.fixSpelling;
} }
@ -41,6 +48,8 @@ enum SmartEditAction {
return LocaleKeys.document_plugins_smartEditSummarize.tr(); return LocaleKeys.document_plugins_smartEditSummarize.tr();
case SmartEditAction.fixSpelling: case SmartEditAction.fixSpelling:
return LocaleKeys.document_plugins_smartEditFixSpelling.tr(); return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
case SmartEditAction.improveWriting:
return LocaleKeys.document_plugins_smartEditImproveWriting.tr();
} }
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/service/op
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -242,7 +242,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
), ),
onPressed: () async { onPressed: () async {
await _onReplace(); await _onReplace();
_onExit(); await _onExit();
}, },
), ),
const Space(10, 0), const Space(10, 0),
@ -257,7 +257,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
), ),
onPressed: () async { onPressed: () async {
await _onInsertBelow(); await _onInsertBelow();
_onExit(); await _onExit();
}, },
), ),
const Space(10, 0), const Space(10, 0),
@ -272,11 +272,14 @@ class _SmartEditInputState extends State<_SmartEditInput> {
), ),
onPressed: () async => await _onExit(), onPressed: () async => await _onExit(),
), ),
const Spacer(), const Spacer(flex: 2),
FlowyText.regular( Expanded(
child: FlowyText.regular(
overflow: TextOverflow.ellipsis,
LocaleKeys.document_plugins_warning.tr(), LocaleKeys.document_plugins_warning.tr(),
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
),
], ],
); );
} }
@ -298,7 +301,22 @@ class _SmartEditInputState extends State<_SmartEditInput> {
selection, selection,
texts, texts,
); );
return widget.editorState.apply(transaction); await widget.editorState.apply(transaction);
int endOffset = texts.last.length;
if (texts.length == 1) {
endOffset += selection.start.offset;
}
await widget.editorState.updateCursorSelection(
Selection(
start: selection.start,
end: Position(
path: [selection.start.path.first + texts.length - 1],
offset: endOffset,
),
),
);
} }
Future<void> _onInsertBelow() async { Future<void> _onInsertBelow() async {
@ -317,7 +335,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
), ),
), ),
); );
return widget.editorState.apply(transaction); await widget.editorState.apply(transaction);
await widget.editorState.updateCursorSelection(
Selection(
start: Position(path: selection.end.path.next, offset: 0),
end: Position(
path: [selection.end.path.next.first + texts.length],
),
),
);
} }
Future<void> _onExit() async { Future<void> _onExit() async {
@ -333,12 +360,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
} }
Future<void> _requestCompletions() async { Future<void> _requestCompletions() async {
final result = await UserBackendService.getCurrentUserProfile(); final openAIRepository = await getIt.getAsync<OpenAIRepository>();
return result.fold((l) async {
final openAIRepository = HttpOpenAIRepository(
client: client,
apiKey: l.openaiKey,
);
var lines = input.split('\n\n'); var lines = input.split('\n\n');
if (action == SmartEditAction.summarize) { if (action == SmartEditAction.summarize) {
@ -356,13 +378,15 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}, },
onProcess: (response) async { onProcess: (response) async {
setState(() { setState(() {
this.result += response.choices.first.text; if (response.choices.first.text != '\n') {
result += response.choices.first.text;
}
}); });
}, },
onEnd: () async { onEnd: () async {
setState(() { setState(() {
if (i != lines.length - 1) { if (i != lines.length - 1) {
this.result += '\n'; result += '\n';
} }
}); });
}, },
@ -372,10 +396,6 @@ class _SmartEditInputState extends State<_SmartEditInput> {
}, },
); );
} }
}, (r) async {
await _showError(r.msg);
await _onExit();
});
} }
Future<void> _showError(String message) async { Future<void> _showError(String message) async {

View File

@ -0,0 +1,21 @@
export 'board/board_node_widget.dart';
export 'board/board_menu_item.dart';
export 'board/board_view_menu_item.dart';
export 'callout/callout_node_widget.dart';
export 'code_block/code_block_node_widget.dart';
export 'code_block/code_block_shortcut_event.dart';
export 'cover/change_cover_popover_bloc.dart';
export 'cover/cover_node_widget.dart';
export 'cover/cover_image_picker.dart';
export 'divider/divider_node_widget.dart';
export 'divider/divider_shortcut_event.dart';
export 'emoji_picker/emoji_menu_item.dart';
export 'extensions/flowy_tint_extension.dart';
export 'grid/grid_menu_item.dart';
export 'grid/grid_node_widget.dart';
export 'grid/grid_view_menu_item.dart';
export 'math_equation/math_equation_node_widget.dart';
export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/auto_completion_plugins.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';

View File

@ -4,10 +4,14 @@ class TrashSizes {
static double get fileNameWidth => 320 * scale; static double get fileNameWidth => 320 * scale;
static double get lashModifyWidth => 230 * scale; static double get lashModifyWidth => 230 * scale;
static double get createTimeWidth => 230 * scale; static double get createTimeWidth => 230 * scale;
static double get padding => 100 * scale; // padding between createTime and action icon
static double get padding => 40 * scale;
static double get actionIconWidth => 40 * scale;
static double get totalWidth => static double get totalWidth =>
TrashSizes.fileNameWidth + TrashSizes.fileNameWidth +
TrashSizes.lashModifyWidth + TrashSizes.lashModifyWidth +
TrashSizes.createTimeWidth + TrashSizes.createTimeWidth +
TrashSizes.padding; TrashSizes.padding +
// restore and delete icon
2 * TrashSizes.actionIconWidth;
} }

View File

@ -38,23 +38,19 @@ class TrashCell extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
FlowyIconButton( FlowyIconButton(
width: 26, iconColorOnHover: Theme.of(context).colorScheme.onSurface,
width: TrashSizes.actionIconWidth,
onPressed: onRestore, onPressed: onRestore,
iconPadding: const EdgeInsets.all(5), iconPadding: const EdgeInsets.all(5),
icon: svgWidget( icon: const FlowySvg(name: 'editor/restore'),
"editor/restore",
color: Theme.of(context).iconTheme.color,
),
), ),
const HSpace(20), const HSpace(20),
FlowyIconButton( FlowyIconButton(
width: 26, iconColorOnHover: Theme.of(context).colorScheme.onSurface,
width: TrashSizes.actionIconWidth,
onPressed: onDelete, onPressed: onDelete,
iconPadding: const EdgeInsets.all(5), iconPadding: const EdgeInsets.all(5),
icon: svgWidget( icon: const FlowySvg(name: 'editor/delete'),
"editor/delete",
color: Theme.of(context).iconTheme.color,
),
), ),
], ],
); );

View File

@ -96,10 +96,7 @@ class _TrashPageState extends State<TrashPage> {
IntrinsicWidth( IntrinsicWidth(
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()), text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()),
leftIcon: svgWidget( leftIcon: const FlowySvg(name: 'editor/restore'),
'editor/restore',
color: Theme.of(context).iconTheme.color,
),
onTap: () => context.read<TrashBloc>().add( onTap: () => context.read<TrashBloc>().add(
const TrashEvent.restoreAll(), const TrashEvent.restoreAll(),
), ),
@ -109,10 +106,7 @@ class _TrashPageState extends State<TrashPage> {
IntrinsicWidth( IntrinsicWidth(
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()), text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()),
leftIcon: svgWidget( leftIcon: const FlowySvg(name: 'editor/delete'),
'editor/delete',
color: Theme.of(context).iconTheme.color,
),
onTap: () => onTap: () =>
context.read<TrashBloc>().add(const TrashEvent.deleteAll()), context.read<TrashBloc>().add(const TrashEvent.deleteAll()),
), ),

View File

@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/file_picker/file_picker_impl.dart'; import 'package:appflowy/util/file_picker/file_picker_impl.dart';
@ -25,6 +26,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
class DependencyResolver { class DependencyResolver {
static Future<void> resolve(GetIt getIt) async { static Future<void> resolve(GetIt getIt) async {
@ -42,8 +44,25 @@ class DependencyResolver {
} }
} }
void _resolveCommonService(GetIt getIt) { void _resolveCommonService(GetIt getIt) async {
getIt.registerFactory<FilePickerService>(() => FilePicker()); getIt.registerFactory<FilePickerService>(() => FilePicker());
getIt.registerFactoryAsync<OpenAIRepository>(
() async {
final result = await UserBackendService.getCurrentUserProfile();
return result.fold(
(l) {
return HttpOpenAIRepository(
client: http.Client(),
apiKey: l.openaiKey,
);
},
(r) {
throw Exception('Failed to get user profile: ${r.msg}');
},
);
},
);
} }
void _resolveUserDeps(GetIt getIt) { void _resolveUserDeps(GetIt getIt) {

View File

@ -28,6 +28,7 @@ class InitAppWidgetTask extends LaunchTask {
EasyLocalization( EasyLocalization(
supportedLocales: const [ supportedLocales: const [
// In alphabetical order // In alphabetical order
Locale('ar', 'AR'),
Locale('ca', 'ES'), Locale('ca', 'ES'),
Locale('de', 'DE'), Locale('de', 'DE'),
Locale('en'), Locale('en'),

View File

@ -225,6 +225,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
secondaryContainer: theme.selector, secondaryContainer: theme.selector,
onSecondaryContainer: theme.topbarBg, onSecondaryContainer: theme.topbarBg,
tertiary: theme.shader7, tertiary: theme.shader7,
// Editor: toolbarColor
onTertiary: theme.toolbarColor,
tertiaryContainer: theme.questionBubbleBG, tertiaryContainer: theme.questionBubbleBG,
background: theme.surface, background: theme.surface,
onBackground: theme.text, onBackground: theme.text,
@ -240,8 +242,15 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
shadow: theme.shadow, shadow: theme.shadow,
); );
const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
MaterialState.pressed,
MaterialState.hovered,
MaterialState.dragged,
};
return ThemeData( return ThemeData(
brightness: brightness, brightness: brightness,
dialogBackgroundColor: theme.surface,
textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text), textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
textSelectionTheme: TextSelectionThemeData( textSelectionTheme: TextSelectionThemeData(
cursorColor: theme.main2, cursorColor: theme.main2,
@ -262,20 +271,20 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
contentTextStyle: TextStyle(color: colorScheme.onSurface), contentTextStyle: TextStyle(color: colorScheme.onSurface),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(theme.shader3), thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.any(scrollbarInteractiveStates.contains)) {
return theme.shader7;
}
return theme.shader5;
}),
thickness: MaterialStateProperty.resolveWith((states) { thickness: MaterialStateProperty.resolveWith((states) {
const Set<MaterialState> interactiveStates = <MaterialState>{ if (states.any(scrollbarInteractiveStates.contains)) {
MaterialState.pressed, return 4;
MaterialState.hovered,
MaterialState.dragged,
};
if (states.any(interactiveStates.contains)) {
return 5.0;
} }
return 3.0; return 3.0;
}), }),
crossAxisMargin: 0.0, crossAxisMargin: 0.0,
mainAxisMargin: 0.0, mainAxisMargin: 6.0,
radius: Corners.s10Radius, radius: Corners.s10Radius,
), ),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
@ -308,7 +317,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
greySelect: theme.bg3, greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3, lightGreyHover: theme.hoverBG3,
toggleOffFill: theme.shader5, toggleOffFill: theme.shader5,
progressBarBGcolor: theme.progressBarBGcolor, progressBarBGColor: theme.progressBarBGColor,
toggleButtonBGColor: theme.toggleButtonBGColor,
code: _getFontStyle( code: _getFontStyle(
fontFamily: monospaceFontFamily, fontFamily: monospaceFontFamily,
fontColor: theme.shader3, fontColor: theme.shader3,

View File

@ -25,6 +25,8 @@ class SettingsLocation {
if (Platform.isMacOS) { if (Platform.isMacOS) {
// remove the prefix `/Volumes/*` // remove the prefix `/Volumes/*`
return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), ''); return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
} else if (Platform.isWindows) {
return _path?.replaceAll("/", "\\");
} }
return _path; return _path;
} }

View File

@ -77,8 +77,8 @@ class ThemeSetting extends StatelessWidget {
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(theme), text: FlowyText.medium(theme),
rightIcon: currentTheme == theme rightIcon: currentTheme == theme
? svgWidget("grid/checkmark") ? const FlowySvg(name: 'grid/checkmark')
: const SizedBox(), : null,
onTap: () { onTap: () {
if (currentTheme != theme) { if (currentTheme != theme) {
context.read<AppearanceSettingsCubit>().setTheme(theme); context.read<AppearanceSettingsCubit>().setTheme(theme);
@ -134,8 +134,8 @@ class ThemeModeSetting extends StatelessWidget {
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(_themeModeLabelText(themeMode)), text: FlowyText.medium(_themeModeLabelText(themeMode)),
rightIcon: currentThemeMode == themeMode rightIcon: currentThemeMode == themeMode
? svgWidget("grid/checkmark") ? const FlowySvg(name: 'grid/checkmark')
: const SizedBox(), : null,
onTap: () { onTap: () {
if (currentThemeMode != themeMode) { if (currentThemeMode != themeMode) {
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode); context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);

View File

@ -35,9 +35,8 @@ class SettingsFileLocationCustomzierState
child: BlocBuilder<SettingsLocationCubit, SettingsLocation>( child: BlocBuilder<SettingsLocationCubit, SettingsLocation>(
builder: (context, state) { builder: (context, state) {
return ListTile( return ListTile(
title: FlowyText.regular( title: FlowyText.medium(
LocaleKeys.settings_files_defaultLocation.tr(), LocaleKeys.settings_files_defaultLocation.tr(),
fontSize: 15.0,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: Tooltip( subtitle: Tooltip(
@ -63,7 +62,6 @@ class SettingsFileLocationCustomzierState
}, },
child: FlowyText.regular( child: FlowyText.regular(
state.path ?? '', state.path ?? '',
fontSize: 10.0,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -74,7 +72,11 @@ class SettingsFileLocationCustomzierState
Tooltip( Tooltip(
message: LocaleKeys.settings_files_restoreLocation.tr(), message: LocaleKeys.settings_files_restoreLocation.tr(),
child: FlowyIconButton( child: FlowyIconButton(
height: 40,
width: 40,
icon: const Icon(Icons.restore_outlined), icon: const Icon(Icons.restore_outlined),
hoverColor:
Theme.of(context).colorScheme.secondaryContainer,
onPressed: () async { onPressed: () async {
final result = await appFlowyDocumentDirectory(); final result = await appFlowyDocumentDirectory();
await _setCustomLocation(result.path); await _setCustomLocation(result.path);
@ -96,7 +98,11 @@ class SettingsFileLocationCustomzierState
Tooltip( Tooltip(
message: LocaleKeys.settings_files_customizeLocation.tr(), message: LocaleKeys.settings_files_customizeLocation.tr(),
child: FlowyIconButton( child: FlowyIconButton(
height: 40,
width: 40,
icon: const Icon(Icons.folder_open_outlined), icon: const Icon(Icons.folder_open_outlined),
hoverColor:
Theme.of(context).colorScheme.secondaryContainer,
onPressed: () async { onPressed: () async {
final result = final result =
await getIt<FilePickerService>().getDirectoryPath(); await getIt<FilePickerService>().getDirectoryPath();

Some files were not shown because too many files have changed in this diff Show More