Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d1189048 | ||
|
|
757009a97d | ||
|
|
e8665dc76c | ||
|
|
f022f9aa06 | ||
|
|
32650b7941 | ||
|
|
5a7339b092 | ||
|
|
7ca028942c | ||
|
|
6fbd88fe76 | ||
|
|
2b5ecf5609 | ||
|
|
18672b197c | ||
|
|
63a267ca6d | ||
|
|
1f1a97b21b | ||
|
|
ca1ce8e19e | ||
|
|
f8f0599462 | ||
|
|
a4845e668d | ||
|
|
dd5bc7808c | ||
|
|
a993a2a181 | ||
|
|
99ad9dcaff | ||
|
|
d44de62b90 | ||
|
|
33b5837f04 | ||
|
|
54f757d9b2 | ||
|
|
cf27409c8d | ||
|
|
bd5250ae2f | ||
|
|
f0a4b4b77d | ||
|
|
151ee89855 | ||
|
|
2b7282195b | ||
|
|
4a1714b537 | ||
|
|
6731037a8c | ||
|
|
06f056aa4b | ||
|
|
a2e9d63f63 | ||
|
|
5d0a349236 | ||
|
|
db8d030a85 | ||
|
|
7aac4bc90b | ||
|
|
8f41a1023c | ||
|
|
5e2b090f46 | ||
|
|
d65a56151b | ||
|
|
c672a2a8ba | ||
|
|
75262c0683 | ||
|
|
39b1ff0910 | ||
|
|
f9095cfc64 | ||
|
|
9e3ad1a6be | ||
|
|
473eb8bcbe | ||
|
|
22f09ddcb7 | ||
|
|
2e18c020bc | ||
|
|
c320f6ef8a | ||
|
|
8cacfb1d07 | ||
|
|
c4ff1a6290 | ||
|
|
a48fca159f | ||
|
|
95bc325e85 | ||
|
|
d5b70c842b | ||
|
|
545e57448a | ||
|
|
3277f179ef | ||
|
|
c6cd10dfcf | ||
|
|
4be98fdba0 | ||
|
|
df4a5ca059 | ||
|
|
77d58a81fd | ||
|
|
ac07c259f1 | ||
|
|
cd1202d3dc | ||
|
|
28f22e4d61 | ||
|
|
2b1b1c0f28 | ||
|
|
c8f7401470 | ||
|
|
37ba5d8e43 | ||
|
|
857cd9ff55 | ||
|
|
fccd6135f7 | ||
|
|
49caf74ee1 | ||
|
|
aaa3eb9041 | ||
|
|
9e47603360 | ||
|
|
71bff8f864 | ||
|
|
2af2621b49 | ||
|
|
a604c0f238 |
No files matched your search
@@ -7,6 +7,7 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -14,6 +15,7 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.7.5"
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-10.15,
|
||||
os: macos-11,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
@@ -236,14 +236,6 @@ jobs:
|
||||
extra-build-args: "",
|
||||
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:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -13,4 +13,6 @@ jobs:
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
with:
|
||||
args: '@appflowytranslators English UI strings has been updated.'
|
||||
args: |
|
||||
@appflowytranslators English UI strings has been updated.
|
||||
Link to changes: ${{github.event.compare}}
|
||||
@@ -1,5 +1,32 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.1.6 - 30/05/2023
|
||||
|
||||
### New features
|
||||
- Support exporting your document and database data.
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
### Bug Fixes
|
||||
- Fix: calendar dates don't match with weekdays.
|
||||
- Fix: sort numbers in Grid.
|
||||
|
||||
## Version 0.1.4 - 04/05/2023
|
||||
|
||||
### New features
|
||||
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
|
||||
- Writing can be improved with the help of OpenAI.
|
||||
|
||||
## Version 0.1.3 - 24/04/2023
|
||||
|
||||
### New features
|
||||
- Launch the official Dark Mode.
|
||||
- Customize the font color and highlight color by setting a hex color value and an opacity level.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix: the slash menu can be triggered by all other keyboards than English.
|
||||
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
|
||||
|
||||
## Version 0.1.2 - 03/28/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -23,9 +23,11 @@ You are in charge of your data and customizations.
|
||||
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/200787830-96be260b-d0a0-4152-864e-6730b19095cd.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174753177-98e4c899-2356-4137-bb42-374bba2b127b.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/190650183-a940f1e0-a2c5-4797-ab3a-56758f6f696c.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664610-fc209a97-815e-4716-af07-d94a859d1907.png" alt="AppFlowy Docs & Notes & Wikis" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664628-5def2450-914a-4b2d-b907-92b7476b9863.png" alt="AppFlowy Databases for Tasks and Projects" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664642-22e26c1b-5eae-4635-9aa6-b12ecf1c3c46.png" alt="AppFlowy Kanban Board for To-Dos" width="1000px" /></p>
|
||||
<p align="center"><img src="https://github.com/AppFlowy-IO/AppFlowy/assets/12026239/6be93d2b-a5c5-48a9-b7cf-c599d5f5140c" alt="AppFlowy Calendars for Plan and Manage Content" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664657-dc5291f3-67b0-4a43-a818-640e92735deb.png" alt="AppFlowy OpenAI GPT Writers" width="1000px" /></p>
|
||||
|
||||
## User Installation
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.1.2"
|
||||
CURRENT_APP_VERSION = "0.1.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Binary file not shown.
@@ -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": "تقويم التخطيط بواسطة"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@
|
||||
"medium": "medium",
|
||||
"large": "large",
|
||||
"fontSize": "Font Size",
|
||||
"import": "Import"
|
||||
"import": "Import",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
@@ -74,6 +75,7 @@
|
||||
"shortcuts": "Shortcuts",
|
||||
"whatsNew": "What's new?",
|
||||
"help": "Help & Support",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "Debug Info",
|
||||
"success": "Copied debug info to clipboard!",
|
||||
@@ -182,6 +184,7 @@
|
||||
},
|
||||
"files": {
|
||||
"defaultLocation": "Where your data is stored now",
|
||||
"exportData": "Export your data",
|
||||
"doubleTapToCopy": "Double tap to copy the path",
|
||||
"restoreLocation": "Restore to AppFlowy default path",
|
||||
"customizeLocation": "Open another folder",
|
||||
@@ -200,7 +203,9 @@
|
||||
"create": "Create",
|
||||
"folderPath": "Path to store your folder",
|
||||
"locationCannotBeEmpty": "Path cannot be empty",
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!"
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!"
|
||||
},
|
||||
"user": {
|
||||
"name": "Name",
|
||||
@@ -279,7 +284,7 @@
|
||||
"numberFormat": "Number format",
|
||||
"dateFormat": "Date format",
|
||||
"includeTime": "Include time",
|
||||
"dateFormatFriendly": "Month Day,Year",
|
||||
"dateFormatFriendly": "Month Day, Year",
|
||||
"dateFormatISO": "Year-Month-Day",
|
||||
"dateFormatLocal": "Month/Day/Year",
|
||||
"dateFormatUS": "Year/Month/Day",
|
||||
@@ -307,7 +312,8 @@
|
||||
"textPlaceholder": "Empty",
|
||||
"copyProperty": "Copied property to clipboard",
|
||||
"count": "Count",
|
||||
"newRow": "New row"
|
||||
"newRow": "New row",
|
||||
"action": "Action"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Create",
|
||||
@@ -361,6 +367,8 @@
|
||||
"smartEditFixSpelling": "Fix spelling",
|
||||
"warning": "⚠️ AI responses can be inaccurate or misleading.",
|
||||
"smartEditSummarize": "Summarize",
|
||||
"smartEditImproveWriting": "Improve writing",
|
||||
"smartEditMakeLonger": "Make longer",
|
||||
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
@@ -414,8 +422,10 @@
|
||||
"settings": {
|
||||
"showWeekNumbers": "Show week numbers",
|
||||
"showWeekends": "Show weekends",
|
||||
"firstDayOfWeek": "First day of week",
|
||||
"layoutDateField": "Layout calendar by"
|
||||
"firstDayOfWeek": "Start week on",
|
||||
"layoutDateField": "Layout calendar by",
|
||||
"noDateTitle": "No Date",
|
||||
"emptyNoDate": "No unscheduled events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@
|
||||
"numberFormat": "Format angka",
|
||||
"dateFormat": "Format tanggal",
|
||||
"includeTime": "Sertakan waktu",
|
||||
"dateFormatFriendly": "Bulan Hari,Tahun",
|
||||
"dateFormatFriendly": "Bulan Hari, Tahun",
|
||||
"dateFormatISO": "Tahun-Bulan-Hari",
|
||||
"dateFormatLocal": "Bulan/Hari/Tahun",
|
||||
"dateFormatUS": "Tahun/Bulan/Hari",
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
"numberFormat": "数値書式",
|
||||
"dateFormat": "日付書式",
|
||||
"includeTime": "時刻を含める",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatFriendly": "月 日, 年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
"numberFormat": "숫자 형식",
|
||||
"dateFormat": "날짜 형식",
|
||||
"includeTime": "시간 표시",
|
||||
"dateFormatFriendly": "월 일,년",
|
||||
"dateFormatFriendly": "월 일, 년",
|
||||
"dateFormatISO": "년-월-일",
|
||||
"dateFormatLocal": "월/일/년",
|
||||
"dateFormatUS": "년/월/일",
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
"numberFormat": "Formato numérico",
|
||||
"dateFormat": "Formato de data",
|
||||
"includeTime": "Incluir hora",
|
||||
"dateFormatFriendly": "Mês Dia,Ano",
|
||||
"dateFormatFriendly": "Mês Dia, Ano",
|
||||
"dateFormatISO": "Ano-Mês-Dia",
|
||||
"dateFormatLocal": "Mês/Dia/Ano",
|
||||
"dateFormatUS": "Ano/Mês/Dia",
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"small": "маленький",
|
||||
"medium": "средний",
|
||||
"large": "большой",
|
||||
"fontSize": "Размер шрифта"
|
||||
"fontSize": "Размер шрифта",
|
||||
"import": "Импортировать"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Переименовать",
|
||||
@@ -70,8 +71,10 @@
|
||||
},
|
||||
"dialogCreatePageNameHint": "Имя страницы",
|
||||
"questionBubble": {
|
||||
"shortcuts": "Комбинации клавиш",
|
||||
"whatsNew": "Что нового?",
|
||||
"help": "Помощь",
|
||||
"markdown": "Markdown",
|
||||
"debug": {
|
||||
"name": "Отладочная информация",
|
||||
"success": "Скопировано в буфер обмена!",
|
||||
@@ -126,6 +129,7 @@
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Done": "Завершить",
|
||||
"Cancel": "Отмена",
|
||||
"signIn": "Войти",
|
||||
"signOut": "Выйти",
|
||||
@@ -170,7 +174,7 @@
|
||||
},
|
||||
"appearance": {
|
||||
"themeMode": {
|
||||
"label": "Режим темы",
|
||||
"label": "Тема приложения",
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
"system": "Системная"
|
||||
@@ -197,7 +201,7 @@
|
||||
"create": "Создать",
|
||||
"folderPath": "Путь к вашей папке",
|
||||
"locationCannotBeEmpty": "Путь не может быть пустым",
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!"
|
||||
"pathCopiedSnackbar": "Путь скопирован в буфер обмена!"
|
||||
},
|
||||
"user": {
|
||||
"name": "Имя",
|
||||
@@ -216,7 +220,8 @@
|
||||
"addFilter": "Добавить фильтр",
|
||||
"deleteFilter": "Удалить фильтр",
|
||||
"filterBy": "Фильтровать по...",
|
||||
"typeAValue": "Введите значение..."
|
||||
"typeAValue": "Введите значение...",
|
||||
"layout": "Раскладка"
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "Содержит",
|
||||
@@ -303,7 +308,8 @@
|
||||
"textPlaceholder": "Пусто",
|
||||
"copyProperty": "Свойство скопировано",
|
||||
"count": "Количество",
|
||||
"newRow": "Новая строка"
|
||||
"newRow": "Новая строка",
|
||||
"action": "Действия"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Создать",
|
||||
@@ -384,6 +390,10 @@
|
||||
"addIcon": "Добавить иконку",
|
||||
"coverRemoveAlert": "Изображение будет удалено с обложки",
|
||||
"alertDialogConfirmation": "Вы хотите продолжить?"
|
||||
},
|
||||
"mathEquation": {
|
||||
"addMathEquation": "Добавить математическое выражение",
|
||||
"editMathEquation": "Редактировать математическое выражение"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"numberFormat": "Sifferformat",
|
||||
"dateFormat": "Datumformat",
|
||||
"includeTime": "Inkludera tid",
|
||||
"dateFormatFriendly": "Månad Dag,År",
|
||||
"dateFormatFriendly": "Månad Dag, År",
|
||||
"dateFormatISO": "År-Månad-Dag",
|
||||
"dateFormatLocal": "Månad/Dag/År",
|
||||
"dateFormatUS": "År/Månad/Dag",
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"numberFormat": "数字格式",
|
||||
"dateFormat": "日期格式",
|
||||
"includeTime": "包含时间",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatFriendly": "月 日, 年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
"numberFormat": "數字格式",
|
||||
"dateFormat": "日期格式",
|
||||
"includeTime": "包含時間",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatFriendly": "月 日, 年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
|
||||
@@ -34,6 +34,8 @@ void main() {
|
||||
group('board', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets(
|
||||
'integration test unzips the proper workspace and loads it correctly.',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -30,6 +30,8 @@ void main() {
|
||||
group('Tests on a workspace with only an empty document', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets('/board shortcut creates a new board and view of the board',
|
||||
(tester) async {
|
||||
@@ -69,6 +71,7 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
expect(find.byType(BuiltInPageWidget), findsOneWidget);
|
||||
@@ -113,7 +116,12 @@ void main() {
|
||||
expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
|
||||
|
||||
// Finalizes the slash command that creates the board.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.enter,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'board_test.dart' as board_test;
|
||||
import 'switch_folder_test.dart' as switch_folder_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.
|
||||
///
|
||||
@@ -16,4 +17,5 @@ void main() {
|
||||
switch_folder_test.main();
|
||||
board_test.main();
|
||||
empty_document_test.main();
|
||||
smart_menu_test.main();
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum TestWorkspace {
|
||||
board("board"),
|
||||
emptyDocument("empty_document");
|
||||
emptyDocument("empty_document"),
|
||||
aiWorkSpace("ai_workspace"),
|
||||
coverImage("cover_image");
|
||||
|
||||
const TestWorkspace(this._name);
|
||||
|
||||
@@ -37,6 +39,13 @@ enum TestWorkspace {
|
||||
return root;
|
||||
}
|
||||
|
||||
Future<void> clean() async {
|
||||
final Directory workspaceRoot = await root;
|
||||
if (await workspaceRoot.exists()) {
|
||||
workspaceRoot.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
String get _asset => 'assets/test/workspaces/$_name.zip';
|
||||
}
|
||||
|
||||
@@ -65,4 +74,12 @@ class TestWorkspaceService {
|
||||
await TestWorkspace._parent.then((value) => value.path),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tearDown() async {
|
||||
await workspace.clean();
|
||||
}
|
||||
|
||||
Future<void> tearDownAll() async {
|
||||
await SharedPreferences.getInstance().then((value) => value.remove(kSettingsLocationDefaultLocation));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export 'target_platform.dart';
|
||||
export 'url_validator.dart';
|
||||
@@ -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;
|
||||
}
|
||||
+1
-4
@@ -76,10 +76,7 @@ class CellController<T, D> extends Equatable {
|
||||
_cellListener?.start(
|
||||
onCellChanged: (result) {
|
||||
result.fold(
|
||||
(_) {
|
||||
_cellCache.remove(_cacheKey);
|
||||
_loadData();
|
||||
},
|
||||
(_) => _loadData(),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
|
||||
+9
-3
@@ -115,7 +115,7 @@ class DatabaseController {
|
||||
}
|
||||
}
|
||||
|
||||
void addListener({
|
||||
void setListener({
|
||||
DatabaseCallbacks? onDatabaseChanged,
|
||||
LayoutCallbacks? onLayoutChanged,
|
||||
GroupCallbacks? onGroupChanged,
|
||||
@@ -211,6 +211,11 @@ class DatabaseController {
|
||||
await _databaseViewBackendSvc.closeView();
|
||||
await fieldController.dispose();
|
||||
await groupListener.stop();
|
||||
await _viewCache.dispose();
|
||||
_databaseCallbacks = null;
|
||||
_groupCallbacks = null;
|
||||
_layoutCallbacks = null;
|
||||
_calendarLayoutCallbacks = null;
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
@@ -251,7 +256,7 @@ class DatabaseController {
|
||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||
},
|
||||
);
|
||||
_viewCache.addListener(callbacks);
|
||||
_viewCache.setListener(callbacks);
|
||||
}
|
||||
|
||||
void _listenOnFieldsChanged() {
|
||||
@@ -336,9 +341,10 @@ class RowDataBuilder {
|
||||
_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) {
|
||||
assert(fieldInfo.fieldType == FieldType.DateTime);
|
||||
final timestamp = (date.millisecondsSinceEpoch ~/ 1000);
|
||||
final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000);
|
||||
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -111,9 +111,10 @@ class DatabaseViewCache {
|
||||
Future<void> dispose() async {
|
||||
await _databaseViewListener.stop();
|
||||
await _rowCache.dispose();
|
||||
_callbacks = null;
|
||||
}
|
||||
|
||||
void addListener(DatabaseViewCallbacks callbacks) {
|
||||
void setListener(DatabaseViewCallbacks callbacks) {
|
||||
_callbacks = callbacks;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -236,7 +236,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
},
|
||||
);
|
||||
|
||||
_databaseController.addListener(
|
||||
_databaseController.setListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onGroupChanged: onGroupChanged,
|
||||
);
|
||||
|
||||
+5
-5
@@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget {
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
late AppFlowyBoardScrollController scrollManager;
|
||||
final cardConfiguration = CardConfiguration<String>();
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
|
||||
final config = const AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Color(0xffF7F8FC),
|
||||
@@ -87,7 +87,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
@override
|
||||
void initState() {
|
||||
scrollManager = AppFlowyBoardScrollController();
|
||||
cardConfiguration.addSelectOptionHook((options, groupId) {
|
||||
renderHook.addSelectOptionHook((options, groupId, _) {
|
||||
// The cell should hide if the option id is equal to the groupId.
|
||||
final isInGroup =
|
||||
options.where((element) => element.id == groupId).isNotEmpty;
|
||||
@@ -254,15 +254,15 @@ class _BoardContentState extends State<BoardContent> {
|
||||
key: ValueKey(groupItemId),
|
||||
margin: config.cardPadding,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: Card<String>(
|
||||
child: RowCard<String>(
|
||||
row: rowPB,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: groupData.group.groupId,
|
||||
fieldId: groupItem.fieldInfo.id,
|
||||
groupingFieldId: groupItem.fieldInfo.id,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
configuration: cardConfiguration,
|
||||
renderHook: renderHook,
|
||||
openCard: (context) => _openCard(
|
||||
viewId,
|
||||
fieldController,
|
||||
|
||||
+106
-51
@@ -46,7 +46,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
emit(state.copyWith(database: Some(database)));
|
||||
},
|
||||
didLoadAllEvents: (events) {
|
||||
emit(state.copyWith(initialEvents: events, allEvents: events));
|
||||
final calenderEvents = _calendarEventDataFromEventPBs(events);
|
||||
emit(
|
||||
state.copyWith(
|
||||
initialEvents: calenderEvents,
|
||||
allEvents: calenderEvents,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) {
|
||||
_loadAllEvents();
|
||||
@@ -55,6 +61,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
createEvent: (DateTime date, String title) async {
|
||||
await _createEvent(date, title);
|
||||
},
|
||||
didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
|
||||
emit(
|
||||
state.copyWith(editEvent: event),
|
||||
);
|
||||
},
|
||||
updateCalendarLayoutSetting:
|
||||
(CalendarLayoutSettingsPB layoutSetting) async {
|
||||
await _updateCalendarLayoutSetting(layoutSetting);
|
||||
@@ -62,7 +73,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
|
||||
var allEvents = [...state.allEvents];
|
||||
final index = allEvents.indexWhere(
|
||||
(element) => element.event!.cellId == eventData.event!.cellId,
|
||||
(element) => element.event!.eventId == eventData.event!.eventId,
|
||||
);
|
||||
if (index != -1) {
|
||||
allEvents[index] = eventData;
|
||||
@@ -70,22 +81,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: allEvents,
|
||||
updateEvent: eventData,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: [...state.allEvents, event],
|
||||
newEvent: event,
|
||||
),
|
||||
);
|
||||
},
|
||||
didDeleteEvents: (List<String> deletedRowIds) {
|
||||
var events = [...state.allEvents];
|
||||
events.retainWhere(
|
||||
(element) => !deletedRowIds.contains(element.event!.cellId.rowId),
|
||||
(element) => !deletedRowIds.contains(element.event!.eventId),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -94,11 +96,25 @@ 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) {
|
||||
final fieldInfos = _databaseController.fieldController.fieldInfos;
|
||||
final index = fieldInfos.indexWhere(
|
||||
@@ -142,17 +158,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
|
||||
final titleField = _getTitleFieldInfo();
|
||||
if (dateField != null && titleField != null) {
|
||||
final result = await _databaseController.createRow(
|
||||
final newRow = await _databaseController.createRow(
|
||||
withCells: (builder) {
|
||||
builder.insertDate(dateField, date);
|
||||
builder.insertText(titleField, title);
|
||||
},
|
||||
).then(
|
||||
(result) => result.fold(
|
||||
(newRow) => newRow,
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(newRow) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
if (newRow != null) {
|
||||
final event = await _loadEvent(newRow.id);
|
||||
if (event != null && !isClosed) {
|
||||
add(CalendarEvent.didCreateEvent(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -186,15 +212,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
result.fold(
|
||||
(events) {
|
||||
if (!isClosed) {
|
||||
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
|
||||
for (final eventPB in events.items) {
|
||||
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
|
||||
if (calendarEvent != null) {
|
||||
calendarEvents.add(calendarEvent);
|
||||
}
|
||||
}
|
||||
|
||||
add(CalendarEvent.didLoadAllEvents(calendarEvents));
|
||||
add(CalendarEvent.didLoadAllEvents(events.items));
|
||||
}
|
||||
},
|
||||
(r) => Log.error(r),
|
||||
@@ -202,22 +220,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(
|
||||
CalendarEventPB eventPB,
|
||||
) {
|
||||
final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
|
||||
final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];
|
||||
if (fieldInfo != null) {
|
||||
final cellId = CellIdentifier(
|
||||
viewId: viewId,
|
||||
rowId: eventPB.rowId,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
|
||||
final eventData = CalendarDayEvent(
|
||||
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(
|
||||
eventPB.timestamp.toInt() * 1000,
|
||||
);
|
||||
@@ -242,25 +270,29 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
|
||||
};
|
||||
},
|
||||
onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
|
||||
onRowsCreated: ((ids) async {
|
||||
for (final id in ids) {
|
||||
onRowsCreated: ((rowIds) async {
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
if (event != null && !isClosed) {
|
||||
add(CalendarEvent.didReceiveNewEvent(event));
|
||||
add(CalendarEvent.didReceiveEvent(event));
|
||||
}
|
||||
}
|
||||
}),
|
||||
onRowsDeleted: (ids) {
|
||||
onRowsDeleted: (rowIds) {
|
||||
if (isClosed) return;
|
||||
add(CalendarEvent.didDeleteEvents(ids));
|
||||
add(CalendarEvent.didDeleteEvents(rowIds));
|
||||
},
|
||||
onRowsUpdated: (ids) async {
|
||||
onRowsUpdated: (rowIds) async {
|
||||
if (isClosed) return;
|
||||
for (final id in ids) {
|
||||
for (final id in rowIds) {
|
||||
final event = await _loadEvent(id);
|
||||
if (event != null) {
|
||||
add(CalendarEvent.didUpdateEvent(event));
|
||||
if (event != null && isEventDayChanged(event)) {
|
||||
if (isEventDayChanged(event)) {
|
||||
add(CalendarEvent.didDeleteEvents([id]));
|
||||
add(CalendarEvent.didReceiveEvent(event));
|
||||
} else {
|
||||
add(CalendarEvent.didUpdateEvent(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -275,7 +307,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
onCalendarLayoutChanged: _didReceiveNewLayoutField,
|
||||
);
|
||||
|
||||
_databaseController.addListener(
|
||||
_databaseController.setListener(
|
||||
onDatabaseChanged: onDatabaseChanged,
|
||||
onLayoutChanged: onLayoutChanged,
|
||||
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
|
||||
@@ -295,6 +327,19 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
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>>;
|
||||
@@ -309,7 +354,7 @@ class CalendarEvent with _$CalendarEvent {
|
||||
) = _ReceiveCalendarSettings;
|
||||
|
||||
// Called after loading all the current evnets
|
||||
const factory CalendarEvent.didLoadAllEvents(Events events) =
|
||||
const factory CalendarEvent.didLoadAllEvents(List<CalendarEventPB> events) =
|
||||
_ReceiveCalendarEvents;
|
||||
|
||||
// Called when specific event was updated
|
||||
@@ -318,10 +363,15 @@ class CalendarEvent with _$CalendarEvent {
|
||||
) = _DidUpdateEvent;
|
||||
|
||||
// Called after creating a new event
|
||||
const factory CalendarEvent.didReceiveNewEvent(
|
||||
const factory CalendarEvent.didCreateEvent(
|
||||
CalendarEventData<CalendarDayEvent> event,
|
||||
) = _DidReceiveNewEvent;
|
||||
|
||||
// Called when receive a new event
|
||||
const factory CalendarEvent.didReceiveEvent(
|
||||
CalendarEventData<CalendarDayEvent> event,
|
||||
) = _DidReceiveEvent;
|
||||
|
||||
// Called when deleting events
|
||||
const factory CalendarEvent.didDeleteEvents(List<String> rowIds) =
|
||||
_DidDeleteEvents;
|
||||
@@ -347,11 +397,12 @@ class CalendarEvent with _$CalendarEvent {
|
||||
class CalendarState with _$CalendarState {
|
||||
const factory CalendarState({
|
||||
required Option<DatabasePB> database,
|
||||
// events by row id
|
||||
required Events allEvents,
|
||||
required Events initialEvents,
|
||||
CalendarEventData<CalendarDayEvent>? editEvent,
|
||||
CalendarEventData<CalendarDayEvent>? newEvent,
|
||||
required List<String> deleteEventIds,
|
||||
CalendarEventData<CalendarDayEvent>? updateEvent,
|
||||
required Option<CalendarLayoutSettingsPB> settings,
|
||||
required DatabaseLoadingState loadingState,
|
||||
required Option<FlowyError> noneOrError,
|
||||
@@ -388,8 +439,12 @@ class CalendarEditingRow {
|
||||
|
||||
class CalendarDayEvent {
|
||||
final CalendarEventPB event;
|
||||
final CellIdentifier cellId;
|
||||
final String dateFieldId;
|
||||
final String eventId;
|
||||
|
||||
String get eventId => cellId.rowId;
|
||||
CalendarDayEvent({required this.cellId, required this.event});
|
||||
CalendarDayEvent({
|
||||
required this.dateFieldId,
|
||||
required this.eventId,
|
||||
required this.event,
|
||||
});
|
||||
}
|
||||
+1
@@ -51,5 +51,6 @@ class CalendarSettingEvent with _$CalendarSettingEvent {
|
||||
}
|
||||
|
||||
enum CalendarSettingAction {
|
||||
properties,
|
||||
layout,
|
||||
}
|
||||
+202
-123
@@ -1,9 +1,9 @@
|
||||
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/widgets/card/card.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/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.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_backend/protobuf/flowy-database/field_entities.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@@ -15,7 +15,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../../widgets/row/cells/select_option_cell/extension.dart';
|
||||
import '../application/calendar_bloc.dart';
|
||||
import 'calendar_page.dart';
|
||||
|
||||
class CalendarDayCard extends StatelessWidget {
|
||||
final String viewId;
|
||||
@@ -23,11 +25,10 @@ class CalendarDayCard extends StatelessWidget {
|
||||
final bool isInMonth;
|
||||
final DateTime date;
|
||||
final RowCache _rowCache;
|
||||
final CardCellBuilder _cellBuilder;
|
||||
final List<CalendarDayEvent> events;
|
||||
final void Function(DateTime) onCreateEvent;
|
||||
|
||||
CalendarDayCard({
|
||||
const CalendarDayCard({
|
||||
required this.viewId,
|
||||
required this.isToday,
|
||||
required this.isInMonth,
|
||||
@@ -37,7 +38,6 @@ class CalendarDayCard extends StatelessWidget {
|
||||
required this.events,
|
||||
Key? key,
|
||||
}) : _rowCache = rowCache,
|
||||
_cellBuilder = CardCellBuilder(rowCache.cellCache),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
@@ -49,79 +49,185 @@ class CalendarDayCard extends StatelessWidget {
|
||||
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => _CardEnterNotifier(),
|
||||
builder: ((context, child) {
|
||||
final children = events.map((event) {
|
||||
return _DayEventCell(
|
||||
event: event,
|
||||
viewId: viewId,
|
||||
onClick: () => _showRowDetailPage(event, context),
|
||||
child: _cellBuilder.buildCell(
|
||||
cellId: event.cellId,
|
||||
styles: {FieldType.RichText: TextCardCellStyle(10)},
|
||||
builder: (context, child) {
|
||||
Widget? multipleCards;
|
||||
if (events.isNotEmpty) {
|
||||
multipleCards = Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) =>
|
||||
_buildCard(context, events[index]),
|
||||
itemCount: events.length,
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _Header(
|
||||
date: date,
|
||||
isInMonth: isInMonth,
|
||||
isToday: isToday,
|
||||
onCreate: () => onCreateEvent(date),
|
||||
),
|
||||
_Header(
|
||||
date: date,
|
||||
isInMonth: isInMonth,
|
||||
isToday: isToday,
|
||||
onCreate: () => onCreateEvent(date),
|
||||
),
|
||||
|
||||
// Add a separator between the header and the content.
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return children[index];
|
||||
},
|
||||
itemCount: children.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
),
|
||||
),
|
||||
|
||||
// Use SizedBox instead of ListView if there are no cards.
|
||||
multipleCards ?? const SizedBox(),
|
||||
],
|
||||
);
|
||||
|
||||
return Container(
|
||||
color: backgroundColor,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: child,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () => onCreateEvent(date),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
|
||||
final dataController = RowController(
|
||||
rowId: event.cellId.rowId,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
Widget _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,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: _rowCache.cellCache,
|
||||
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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
dataController: dataController,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
|
||||
if (selectedOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
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) => showEventDetails(
|
||||
context: context,
|
||||
event: event,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
),
|
||||
styleConfiguration: const RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
cellPadding: EdgeInsets.zero,
|
||||
),
|
||||
renderHook: renderHook,
|
||||
onStartEditing: () {},
|
||||
onEndEditing: () {},
|
||||
);
|
||||
|
||||
return FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,42 +239,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 {
|
||||
final bool isToday;
|
||||
final bool isInMonth;
|
||||
@@ -191,12 +261,16 @@ class _Header extends StatelessWidget {
|
||||
isInMonth: isInMonth,
|
||||
date: date,
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
|
||||
const Spacer(),
|
||||
badge,
|
||||
],
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
|
||||
const Spacer(),
|
||||
badge,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -215,10 +289,8 @@ class _NewEventButton extends StatelessWidget {
|
||||
return FlowyIconButton(
|
||||
onPressed: onClick,
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: svgWidget(
|
||||
"home/add",
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
icon: const FlowySvg(name: "home/add"),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 22,
|
||||
);
|
||||
}
|
||||
@@ -237,31 +309,38 @@ class _DayBadge extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color dayTextColor = Theme.of(context).colorScheme.onSurface;
|
||||
String dayString = date.day == 1
|
||||
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
|
||||
: date.day.toString();
|
||||
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
|
||||
String monthString =
|
||||
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
|
||||
String dayString = date.day.toString();
|
||||
|
||||
if (isToday) {
|
||||
dayTextColor = Theme.of(context).colorScheme.onPrimary;
|
||||
}
|
||||
if (!isInMonth) {
|
||||
dayTextColor = Theme.of(context).disabledColor;
|
||||
}
|
||||
if (isToday) {
|
||||
dayTextColor = Theme.of(context).colorScheme.onPrimary;
|
||||
}
|
||||
|
||||
Widget day = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
color: dayTextColor,
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
if (date.day == 1) FlowyText.medium(monthString),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
width: isToday ? 26 : null,
|
||||
height: isToday ? 26 : null,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
color: dayTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return day;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-14
@@ -9,6 +9,10 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/row/row_cache.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 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_toolbar.dart';
|
||||
@@ -70,19 +74,21 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
},
|
||||
),
|
||||
BlocListener<CalendarBloc, CalendarState>(
|
||||
listenWhen: (p, c) => p.updateEvent != c.updateEvent,
|
||||
listenWhen: (p, c) => p.editEvent != c.editEvent,
|
||||
listener: (context, state) {
|
||||
if (state.updateEvent != null) {
|
||||
_eventController.removeWhere(
|
||||
(element) =>
|
||||
state.updateEvent!.event!.eventId ==
|
||||
element.event!.eventId,
|
||||
if (state.editEvent != null) {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: state.editEvent!.event!,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
_eventController.add(state.updateEvent!);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<CalendarBloc, CalendarState>(
|
||||
// Event create by click the + button or double click on the
|
||||
// calendar
|
||||
listenWhen: (p, c) => p.newEvent != c.newEvent,
|
||||
listener: (context, state) {
|
||||
if (state.newEvent != null) {
|
||||
@@ -116,7 +122,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
cellAspectRatio: .9,
|
||||
cellAspectRatio: .6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
borderColor: Theme.of(context).dividerColor,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
@@ -137,7 +143,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: svgWidget('home/arrow_left'),
|
||||
icon: const FlowySvg(name: 'home/arrow_left'),
|
||||
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.previousPage(),
|
||||
@@ -155,7 +161,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: svgWidget('home/arrow_right'),
|
||||
icon: const FlowySvg(name: 'home/arrow_right'),
|
||||
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.nextPage(),
|
||||
@@ -165,8 +171,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
Widget _headerWeekDayBuilder(day) {
|
||||
// incoming day starts from Monday, the symbols start from Sunday
|
||||
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
||||
final weekDayString = symbols.WEEKDAYS[day];
|
||||
final weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: CalendarSize.daysOfWeekInsets,
|
||||
@@ -185,7 +192,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
isInMonth,
|
||||
) {
|
||||
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(
|
||||
viewId: widget.view.id,
|
||||
isToday: isToday,
|
||||
@@ -205,7 +217,32 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
WeekDays _weekdayFromInt(int dayOfWeek) {
|
||||
// MonthView places the first day of week on the second column for some reason.
|
||||
return WeekDays.values[(dayOfWeek + 1) % 7];
|
||||
// dayOfWeek starts from Sunday, WeekDays starts from Monday
|
||||
return WeekDays.values[(dayOfWeek - 1) % 7];
|
||||
}
|
||||
}
|
||||
|
||||
void showEventDetails({
|
||||
required BuildContext context,
|
||||
required CalendarDayEvent event,
|
||||
required String viewId,
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: rowCache.cellCache,
|
||||
),
|
||||
dataController: dataController,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+8
-5
@@ -217,6 +217,7 @@ class LayoutDateField extends StatelessWidget {
|
||||
direction: PopoverDirection.leftWithTopAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
mutex: popoverMutex,
|
||||
offset: const Offset(-16, 0),
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DatabasePropertyBloc>(
|
||||
@@ -237,9 +238,9 @@ class LayoutDateField extends StatelessWidget {
|
||||
onUpdated(fieldInfo.id);
|
||||
popoverMutex.close();
|
||||
},
|
||||
leftIcon: svgWidget('grid/field/date'),
|
||||
leftIcon: const FlowySvg(name: 'grid/field/date'),
|
||||
rightIcon: fieldInfo.id == fieldId
|
||||
? svgWidget('grid/checkmark')
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
),
|
||||
);
|
||||
@@ -333,12 +334,13 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
direction: PopoverDirection.leftWithTopAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
mutex: popoverMutex,
|
||||
offset: const Offset(-16, 0),
|
||||
popupBuilder: (context) {
|
||||
final symbols =
|
||||
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
||||
// starts from sunday
|
||||
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
|
||||
final index = (entry.key - 1) % 7;
|
||||
final index = entry.key;
|
||||
final string = entry.value;
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
@@ -348,8 +350,9 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
onUpdated(index);
|
||||
popoverMutex.close();
|
||||
},
|
||||
rightIcon:
|
||||
firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
|
||||
rightIcon: firstDayOfWeek == index
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
+25
@@ -2,8 +2,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.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/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -38,6 +40,11 @@ class CalendarSetting extends StatelessWidget {
|
||||
final CalendarSettingAction? action =
|
||||
state.selectedAction.foldLeft(null, (previous, action) => action);
|
||||
switch (action) {
|
||||
case CalendarSettingAction.properties:
|
||||
return GridPropertyList(
|
||||
viewId: settingContext.viewId,
|
||||
fieldController: settingContext.fieldController,
|
||||
);
|
||||
case CalendarSettingAction.layout:
|
||||
return CalendarLayoutSetting(
|
||||
onUpdated: onUpdated,
|
||||
@@ -78,9 +85,16 @@ class AllCalendarSettings extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _settingItem(BuildContext context, CalendarSettingAction action) {
|
||||
Widget? icon;
|
||||
if (action.iconName() != null) {
|
||||
icon = FlowySvg(
|
||||
name: action.iconName()!,
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
leftIcon: icon,
|
||||
text: FlowyText.medium(action.title()),
|
||||
onTap: () {
|
||||
context
|
||||
@@ -93,8 +107,19 @@ class AllCalendarSettings extends StatelessWidget {
|
||||
}
|
||||
|
||||
extension _SettingExtension on CalendarSettingAction {
|
||||
String? iconName() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return 'grid/setting/properties';
|
||||
case CalendarSettingAction.layout:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String title() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return LocaleKeys.grid_settings_Properties.tr();
|
||||
case CalendarSettingAction.layout:
|
||||
return LocaleKeys.grid_settings_layout.tr();
|
||||
}
|
||||
|
||||
+103
-7
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -19,7 +21,8 @@ class CalendarToolbar extends StatelessWidget {
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
children: const [
|
||||
_UnscheduleEventsButton(),
|
||||
_SettingButton(),
|
||||
],
|
||||
),
|
||||
@@ -28,25 +31,22 @@ class CalendarToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SettingButton extends StatefulWidget {
|
||||
const _SettingButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SettingButtonState();
|
||||
}
|
||||
|
||||
class _SettingButtonState extends State<_SettingButton> {
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: FlowyTextButton(
|
||||
@@ -54,7 +54,6 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
onPressed: () => popoverController.show(),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
final bloc = context.watch<CalendarBloc>();
|
||||
@@ -81,3 +80,100 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButton extends StatefulWidget {
|
||||
const _UnscheduleEventsButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_UnscheduleEventsButton> createState() =>
|
||||
_UnscheduleEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
|
||||
late final PopoverController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarBloc, CalendarState>(
|
||||
builder: (context, state) {
|
||||
final unscheduledEvents = state.allEvents
|
||||
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.toList();
|
||||
final viewId = context.read<CalendarBloc>().viewId;
|
||||
final rowCache = context.read<CalendarBloc>().rowCache;
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _controller,
|
||||
offset: const Offset(0, 8),
|
||||
child: FlowyTextButton(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
if (unscheduledEvents.isEmpty) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.calendar_settings_emptyNoDate.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => _UnscheduledEventItem(
|
||||
event: unscheduledEvents[index],
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: unscheduledEvents[index].event!,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
_controller.close();
|
||||
},
|
||||
),
|
||||
itemCount: unscheduledEvents.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduledEventItem extends StatelessWidget {
|
||||
final CalendarEventData<CalendarDayEvent> event;
|
||||
final VoidCallback onPressed;
|
||||
const _UnscheduledEventItem({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyTextButton(
|
||||
event.title,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+18
-7
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -7,31 +8,39 @@ import '../../../application/row/row_data_controller.dart';
|
||||
part 'row_detail_bloc.freezed.dart';
|
||||
|
||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
final RowBackendService rowService;
|
||||
final RowController dataController;
|
||||
|
||||
RowDetailBloc({
|
||||
required this.dataController,
|
||||
}) : super(RowDetailState.initial()) {
|
||||
}) : rowService = RowBackendService(viewId: dataController.viewId),
|
||||
super(RowDetailState.initial()) {
|
||||
on<RowDetailEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
await _startListening();
|
||||
final cells = dataController.loadData();
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
}
|
||||
},
|
||||
didReceiveCellDatas: (_DidReceiveCellDatas value) {
|
||||
emit(state.copyWith(gridCells: value.gridCells));
|
||||
didReceiveCellDatas: (cells) {
|
||||
emit(state.copyWith(gridCells: cells));
|
||||
},
|
||||
deleteField: (_DeleteField value) {
|
||||
deleteField: (fieldId) {
|
||||
final fieldService = FieldBackendService(
|
||||
viewId: dataController.viewId,
|
||||
fieldId: value.fieldId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
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 {
|
||||
const factory RowDetailEvent.initial() = _Initial;
|
||||
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(
|
||||
List<CellIdentifier> gridCells,
|
||||
) = _DidReceiveCellDatas;
|
||||
|
||||
+1
-2
@@ -147,8 +147,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
widget.popoverMutex.listenOnPopoverChanged(() {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
} else {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,6 +203,7 @@ class _DeleteFieldButton extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
final enable = !state.canDelete && !state.isGroupField;
|
||||
Widget button = FlowyButton(
|
||||
disable: !enable,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_delete.tr(),
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
|
||||
+2
-6
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy_popover/appflowy_popover.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:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -58,15 +57,12 @@ class FieldTypeCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
fieldType.title(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
onTap: () => onSelectField(fieldType),
|
||||
leftIcon: svgWidget(
|
||||
fieldType.iconName(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
leftIcon: FlowySvg(
|
||||
name: fieldType.iconName(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+2
-11
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme_extension.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-database/field_entities.pb.dart';
|
||||
@@ -113,20 +112,12 @@ class _SwitchFieldButton extends StatelessWidget {
|
||||
Widget _buildMoreButton(BuildContext context) {
|
||||
final bloc = context.read<FieldTypeOptionEditBloc>();
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
bloc.state.field.fieldType.title(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
leftIcon: svgWidget(
|
||||
bloc.state.field.fieldType.iconName(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightIcon: svgWidget(
|
||||
"grid/more",
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
|
||||
rightIcon: const FlowySvg(name: 'grid/more'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -186,6 +186,7 @@ class CreateFieldButton extends StatelessWidget {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
asBarrier: true,
|
||||
margin: EdgeInsets.zero,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
child: FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
|
||||
+2
-43
@@ -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/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:easy_localization/easy_localization.dart' hide DateFormat;
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@@ -54,7 +52,6 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
const TypeOptionSeparator(),
|
||||
_renderDateFormatButton(context, state.typeOption.dateFormat),
|
||||
_renderTimeFormatButton(context, state.typeOption.timeFormat),
|
||||
const _IncludeTimeButton(),
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
@@ -185,44 +182,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 {
|
||||
final DateFormat selectedFormat;
|
||||
final Function(DateFormat format) onSelected;
|
||||
@@ -274,7 +233,7 @@ class DateFormatCell extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
Widget? checkmark;
|
||||
if (isSelected) {
|
||||
checkmark = svgWidget("grid/checkmark");
|
||||
checkmark = const FlowySvg(name: 'grid/checkmark');
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
@@ -358,7 +317,7 @@ class TimeFormatCell extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
Widget? checkmark;
|
||||
if (isSelected) {
|
||||
checkmark = svgWidget("grid/checkmark");
|
||||
checkmark = const FlowySvg(name: 'grid/checkmark');
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
|
||||
+4
-9
@@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/num
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.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:appflowy_backend/protobuf/flowy-database/format.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -60,15 +59,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
final selectNumUnitButton = SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
rightIcon: svgWidget(
|
||||
"grid/more",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
rightIcon: const FlowySvg(name: 'grid/more'),
|
||||
text: FlowyText.regular(
|
||||
state.typeOption.format.title(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -79,7 +73,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.grid_field_numberFormat.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
@@ -188,7 +181,9 @@ class NumberFormatCell extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
Widget? checkmark;
|
||||
if (isSelected) {
|
||||
checkmark = svgWidget("grid/checkmark");
|
||||
checkmark = const FlowySvg(
|
||||
name: 'grid/checkmark',
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
|
||||
+6
-7
@@ -105,15 +105,10 @@ class _DeleteTag extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
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: () {
|
||||
context
|
||||
.read<EditSelectOptionBloc>()
|
||||
@@ -226,7 +221,11 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(color.optionName()),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
color.optionName(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: colorIcon,
|
||||
rightIcon: checkmark,
|
||||
onTap: () {
|
||||
|
||||
@@ -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/grid/presentation/widgets/row/action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
||||
@@ -13,23 +14,40 @@ import 'card_cell_builder.dart';
|
||||
import 'container/accessory.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 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 bool isEditing;
|
||||
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.viewId,
|
||||
required this.fieldId,
|
||||
this.groupingFieldId,
|
||||
required this.isEditing,
|
||||
required this.rowCache,
|
||||
required this.cellBuilder,
|
||||
@@ -37,15 +55,19 @@ class Card<CustomCardData> extends StatefulWidget {
|
||||
required this.onStartEditing,
|
||||
required this.onEndEditing,
|
||||
this.cardData,
|
||||
this.configuration,
|
||||
this.styleConfiguration = const RowCardStyleConfiguration(
|
||||
showAccessory: true,
|
||||
),
|
||||
this.renderHook,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@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 EditableRowNotifier rowNotifier;
|
||||
late PopoverController popoverController;
|
||||
@@ -56,15 +78,15 @@ class _CardState<T> extends State<Card<T>> {
|
||||
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
||||
_cardBloc = CardBloc(
|
||||
viewId: widget.viewId,
|
||||
groupFieldId: widget.fieldId,
|
||||
groupFieldId: widget.groupingFieldId,
|
||||
isEditing: widget.isEditing,
|
||||
row: widget.row,
|
||||
rowCache: widget.rowCache,
|
||||
)..add(const BoardCardEvent.initial());
|
||||
)..add(const RowCardEvent.initial());
|
||||
|
||||
rowNotifier.isEditing.addListener(() {
|
||||
if (!mounted) return;
|
||||
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
||||
_cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
||||
|
||||
if (rowNotifier.isEditing.value) {
|
||||
widget.onStartEditing();
|
||||
@@ -81,7 +103,7 @@ class _CardState<T> extends State<Card<T>> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cardBloc,
|
||||
child: BlocBuilder<CardBloc, BoardCardState>(
|
||||
child: BlocBuilder<CardBloc, RowCardState>(
|
||||
buildWhen: (previous, current) {
|
||||
// Rebuild when:
|
||||
// 1.If the length of the cells is not the same
|
||||
@@ -106,21 +128,26 @@ class _CardState<T> extends State<Card<T>> {
|
||||
context,
|
||||
popoverContext,
|
||||
),
|
||||
child: BoardCardContainer(
|
||||
child: RowCardContainer(
|
||||
buildAccessoryWhen: () => state.isEditing == false,
|
||||
accessoryBuilder: (context) {
|
||||
return [
|
||||
_CardEditOption(rowNotifier: rowNotifier),
|
||||
_CardMoreOption(),
|
||||
];
|
||||
if (widget.styleConfiguration.showAccessory == false) {
|
||||
return [];
|
||||
} else {
|
||||
return [
|
||||
_CardEditOption(rowNotifier: rowNotifier),
|
||||
_CardMoreOption(),
|
||||
];
|
||||
}
|
||||
},
|
||||
openAccessory: _handleOpenAccessory,
|
||||
openCard: (context) => widget.openCard(context),
|
||||
child: _CardContent<T>(
|
||||
rowNotifier: rowNotifier,
|
||||
cellBuilder: widget.cellBuilder,
|
||||
styleConfiguration: widget.styleConfiguration,
|
||||
cells: state.cells,
|
||||
cardConfiguration: widget.configuration,
|
||||
renderHook: widget.renderHook,
|
||||
cardData: widget.cardData,
|
||||
),
|
||||
),
|
||||
@@ -166,15 +193,17 @@ class _CardState<T> extends State<Card<T>> {
|
||||
class _CardContent<CustomCardData> extends StatelessWidget {
|
||||
final CardCellBuilder<CustomCardData> cellBuilder;
|
||||
final EditableRowNotifier rowNotifier;
|
||||
final List<BoardCellEquatable> cells;
|
||||
final CardConfiguration<CustomCardData>? cardConfiguration;
|
||||
final List<CellIdentifier> cells;
|
||||
final RowCardRenderHook<CustomCardData>? renderHook;
|
||||
final CustomCardData? cardData;
|
||||
final RowCardStyleConfiguration styleConfiguration;
|
||||
const _CardContent({
|
||||
required this.rowNotifier,
|
||||
required this.cellBuilder,
|
||||
required this.cells,
|
||||
required this.cardData,
|
||||
this.cardConfiguration,
|
||||
required this.styleConfiguration,
|
||||
this.renderHook,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -188,30 +217,30 @@ class _CardContent<CustomCardData> extends StatelessWidget {
|
||||
|
||||
List<Widget> _makeCells(
|
||||
BuildContext context,
|
||||
List<BoardCellEquatable> cells,
|
||||
List<CellIdentifier> cells,
|
||||
) {
|
||||
final List<Widget> children = [];
|
||||
// Remove all the cell listeners.
|
||||
rowNotifier.unbind();
|
||||
|
||||
cells.asMap().forEach(
|
||||
(int index, BoardCellEquatable cell) {
|
||||
(int index, CellIdentifier cell) {
|
||||
final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
|
||||
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
|
||||
|
||||
if (index == 0) {
|
||||
// Only use the first cell to receive user's input when click the edit
|
||||
// button
|
||||
rowNotifier.bindCell(cell.identifier, cellNotifier);
|
||||
rowNotifier.bindCell(cell, cellNotifier);
|
||||
}
|
||||
|
||||
final child = Padding(
|
||||
key: cell.identifier.key(),
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
key: cell.key(),
|
||||
padding: styleConfiguration.cellPadding,
|
||||
child: cellBuilder.buildCell(
|
||||
cellId: cell.identifier,
|
||||
cellId: cell,
|
||||
cellNotifier: cellNotifier,
|
||||
cardConfiguration: cardConfiguration,
|
||||
renderHook: renderHook,
|
||||
cardData: cardData,
|
||||
),
|
||||
);
|
||||
@@ -265,3 +294,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
|
||||
@override
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:collection';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -12,9 +11,9 @@ import '../../application/row/row_service.dart';
|
||||
|
||||
part 'card_bloc.freezed.dart';
|
||||
|
||||
class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
||||
final RowPB row;
|
||||
final String groupFieldId;
|
||||
final String? groupFieldId;
|
||||
final RowBackendService _rowBackendSvc;
|
||||
final RowCache _rowCache;
|
||||
VoidCallback? _rowCallback;
|
||||
@@ -28,13 +27,13 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||
_rowCache = rowCache,
|
||||
super(
|
||||
BoardCardState.initial(
|
||||
RowCardState.initial(
|
||||
row,
|
||||
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
|
||||
isEditing,
|
||||
),
|
||||
) {
|
||||
on<BoardCardEvent>(
|
||||
on<RowCardEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
@@ -69,7 +68,7 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
return RowInfo(
|
||||
viewId: _rowBackendSvc.viewId,
|
||||
fields: UnmodifiableListView(
|
||||
state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
|
||||
state.cells.map((cell) => cell.fieldInfo).toList(),
|
||||
),
|
||||
rowPB: state.rowPB,
|
||||
);
|
||||
@@ -81,70 +80,58 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
onCellUpdated: (cellMap, reason) {
|
||||
if (!isClosed) {
|
||||
final cells = _makeCells(groupFieldId, cellMap);
|
||||
add(BoardCardEvent.didReceiveCells(cells, reason));
|
||||
add(RowCardEvent.didReceiveCells(cells, reason));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<BoardCellEquatable> _makeCells(
|
||||
String groupFieldId,
|
||||
List<CellIdentifier> _makeCells(
|
||||
String? groupFieldId,
|
||||
CellByFieldId originalCellMap,
|
||||
) {
|
||||
List<BoardCellEquatable> cells = [];
|
||||
List<CellIdentifier> cells = [];
|
||||
for (final entry in originalCellMap.entries) {
|
||||
// Filter out the cell if it's fieldId equal to the groupFieldId
|
||||
if (entry.value.fieldId != groupFieldId) {
|
||||
cells.add(BoardCellEquatable(entry.value));
|
||||
if (groupFieldId != null) {
|
||||
if (entry.value.fieldId == groupFieldId) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cells.add(entry.value);
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardCardEvent with _$BoardCardEvent {
|
||||
const factory BoardCardEvent.initial() = _InitialRow;
|
||||
const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing;
|
||||
const factory BoardCardEvent.didReceiveCells(
|
||||
List<BoardCellEquatable> cells,
|
||||
class RowCardEvent with _$RowCardEvent {
|
||||
const factory RowCardEvent.initial() = _InitialRow;
|
||||
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
|
||||
const factory RowCardEvent.didReceiveCells(
|
||||
List<CellIdentifier> cells,
|
||||
RowsChangedReason reason,
|
||||
) = _DidReceiveCells;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class BoardCardState with _$BoardCardState {
|
||||
const factory BoardCardState({
|
||||
class RowCardState with _$RowCardState {
|
||||
const factory RowCardState({
|
||||
required RowPB rowPB,
|
||||
required List<BoardCellEquatable> cells,
|
||||
required List<CellIdentifier> cells,
|
||||
required bool isEditing,
|
||||
RowsChangedReason? changeReason,
|
||||
}) = _BoardCardState;
|
||||
}) = _RowCardState;
|
||||
|
||||
factory BoardCardState.initial(
|
||||
factory RowCardState.initial(
|
||||
RowPB rowPB,
|
||||
List<BoardCellEquatable> cells,
|
||||
List<CellIdentifier> cells,
|
||||
bool isEditing,
|
||||
) =>
|
||||
BoardCardState(
|
||||
RowCardState(
|
||||
rowPB: rowPB,
|
||||
cells: cells,
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -15,15 +15,15 @@ import 'cells/url_card_cell.dart';
|
||||
// T represents as the Generic card data
|
||||
class CardCellBuilder<CustomCardData> {
|
||||
final CellCache cellCache;
|
||||
final Map<FieldType, CardCellStyle>? styles;
|
||||
|
||||
CardCellBuilder(this.cellCache);
|
||||
CardCellBuilder(this.cellCache, {this.styles});
|
||||
|
||||
Widget buildCell({
|
||||
CustomCardData? cardData,
|
||||
required CellIdentifier cellId,
|
||||
EditableCardNotifier? cellNotifier,
|
||||
CardConfiguration<CustomCardData>? cardConfiguration,
|
||||
Map<FieldType, CardCellStyle>? styles,
|
||||
RowCardRenderHook<CustomCardData>? renderHook,
|
||||
}) {
|
||||
final cellControllerBuilder = CellControllerBuilder(
|
||||
cellId: cellId,
|
||||
@@ -39,20 +39,21 @@ class CardCellBuilder<CustomCardData> {
|
||||
key: key,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return DateCardCell(
|
||||
return DateCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCardCell<CustomCardData>(
|
||||
renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect],
|
||||
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cardData: cardData,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCardCell<CustomCardData>(
|
||||
renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect],
|
||||
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cardData: cardData,
|
||||
editableNotifier: cellNotifier,
|
||||
@@ -64,19 +65,24 @@ class CardCellBuilder<CustomCardData> {
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return NumberCardCell(
|
||||
return NumberCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.Number],
|
||||
style: isStyleOrNull<NumberCardCellStyle>(style),
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return TextCardCell(
|
||||
return TextCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.RichText],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
editableNotifier: cellNotifier,
|
||||
cardData: cardData,
|
||||
style: isStyleOrNull<TextCardCellStyle>(style),
|
||||
key: key,
|
||||
);
|
||||
case FieldType.URL:
|
||||
return URLCardCell(
|
||||
return URLCardCell<CustomCardData>(
|
||||
style: isStyleOrNull<URLCardCellStyle>(style),
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
|
||||
+54
-9
@@ -1,26 +1,71 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.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>>;
|
||||
|
||||
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 = {};
|
||||
CardConfiguration();
|
||||
RowCardRenderHook();
|
||||
|
||||
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
|
||||
void addSelectOptionHook(
|
||||
CellRenderHook<List<SelectOptionPB>, CustomCardData> hook,
|
||||
CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
|
||||
) {
|
||||
selectOptionHook(cellData, cardData) {
|
||||
if (cellData is List<SelectOptionPB>) {
|
||||
hook(cellData, cardData);
|
||||
final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
|
||||
renderHook[FieldType.SingleSelect] = hookFn;
|
||||
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;
|
||||
renderHook[FieldType.MultiSelect] = selectOptionHook;
|
||||
return hookFn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-7
@@ -44,13 +44,16 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
|
||||
: svgWidget('editor/editor_uncheck');
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyIconButton(
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: icon,
|
||||
width: 20,
|
||||
onPressed: () => context
|
||||
.read<CheckboxCardCellBloc>()
|
||||
.add(const CheckboxCardCellEvent.select()),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: FlowyIconButton(
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: icon,
|
||||
width: 20,
|
||||
onPressed: () => context
|
||||
.read<CheckboxCardCellBloc>()
|
||||
.add(const CheckboxCardCellEvent.select()),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+12
-1
@@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart';
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
class DateCardCell extends CardCell {
|
||||
class DateCardCell<CustomCardData> extends CardCell {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||
|
||||
const DateCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
this.renderHook,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -42,6 +44,15 @@ class _DateCardCellState extends State<DateCardCell> {
|
||||
if (state.dateStr.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
Widget? custom = widget.renderHook?.call(
|
||||
state.data,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
|
||||
+23
-3
@@ -7,13 +7,24 @@ import '../bloc/number_card_cell_bloc.dart';
|
||||
import '../define.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;
|
||||
|
||||
const NumberCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
CustomCardData? cardData,
|
||||
NumberCardCellStyle? style,
|
||||
this.renderHook,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key, style: style, cardData: cardData);
|
||||
|
||||
@override
|
||||
State<NumberCardCell> createState() => _NumberCardCellState();
|
||||
@@ -42,6 +53,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
Widget? custom = widget.renderHook?.call(
|
||||
state.content,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
@@ -50,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
fontSize: 14,
|
||||
fontSize: widget.style?.fontSize ?? 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+5
-3
@@ -11,17 +11,18 @@ import 'card_cell.dart';
|
||||
|
||||
class SelectOptionCardCellStyle extends CardCellStyle {}
|
||||
|
||||
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
|
||||
class SelectOptionCardCell<CustomCardData>
|
||||
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
|
||||
with EditableCell {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
|
||||
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
|
||||
|
||||
@override
|
||||
final EditableCardNotifier? editableNotifier;
|
||||
|
||||
SelectOptionCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
required T? cardData,
|
||||
required CustomCardData? cardData,
|
||||
this.renderHook,
|
||||
this.editableNotifier,
|
||||
Key? key,
|
||||
@@ -57,6 +58,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
|
||||
Widget? custom = widget.renderHook?.call(
|
||||
state.selectedOptions,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
|
||||
+16
-3
@@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle {
|
||||
TextCardCellStyle(this.fontSize);
|
||||
}
|
||||
|
||||
class TextCardCell extends CardCell<String, TextCardCellStyle>
|
||||
with EditableCell {
|
||||
class TextCardCell<CustomCardData>
|
||||
extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
|
||||
@override
|
||||
final EditableCardNotifier? editableNotifier;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<String, CustomCardData>? renderHook;
|
||||
|
||||
const TextCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
required CustomCardData? cardData,
|
||||
this.editableNotifier,
|
||||
this.renderHook,
|
||||
TextCardCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key, style: style);
|
||||
}) : super(key: key, style: style, cardData: cardData);
|
||||
|
||||
@override
|
||||
State<TextCardCell> createState() => _TextCardCellState();
|
||||
@@ -104,6 +107,16 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
return previous != current;
|
||||
},
|
||||
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 &&
|
||||
state.enableEdit == false &&
|
||||
focusWhenInit == false) {
|
||||
|
||||
+11
-3
@@ -8,13 +8,21 @@ import '../bloc/url_card_cell_bloc.dart';
|
||||
import '../define.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;
|
||||
|
||||
const URLCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
URLCardCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key, style: style);
|
||||
|
||||
@override
|
||||
State<URLCardCell> createState() => _URLCardCellState();
|
||||
@@ -55,7 +63,7 @@ class _URLCardCellState extends State<URLCardCell> {
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.size(FontSizes.s14)
|
||||
.size(widget.style?.fontSize ?? FontSizes.s14)
|
||||
.textColor(Theme.of(context).colorScheme.primary)
|
||||
.underline,
|
||||
),
|
||||
|
||||
+3
-2
@@ -4,13 +4,13 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'accessory.dart';
|
||||
|
||||
class BoardCardContainer extends StatelessWidget {
|
||||
class RowCardContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final CardAccessoryBuilder? accessoryBuilder;
|
||||
final bool Function()? buildAccessoryWhen;
|
||||
final void Function(BuildContext) openCard;
|
||||
final void Function(AccessoryType) openAccessory;
|
||||
const BoardCardContainer({
|
||||
const RowCardContainer({
|
||||
required this.child,
|
||||
required this.openCard,
|
||||
required this.openAccessory,
|
||||
@@ -43,6 +43,7 @@ class BoardCardContainer extends StatelessWidget {
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => openCard(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class ChecklistProgressBar extends StatelessWidget {
|
||||
percent: percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGcolor,
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
);
|
||||
}
|
||||
|
||||
+30
-10
@@ -84,15 +84,23 @@ class DateCellCalendarBloc
|
||||
bool? includeTime,
|
||||
}) {
|
||||
final DateCellData newDateData = state.dateCellData.fold(
|
||||
() => DateCellData(
|
||||
date: date ?? DateTime.now(),
|
||||
time: time,
|
||||
includeTime: includeTime ?? false,
|
||||
),
|
||||
() {
|
||||
DateTime newDate = DateTime.now();
|
||||
if (date != null) {
|
||||
newDate = _utcToLocalAddTime(date);
|
||||
}
|
||||
return DateCellData(
|
||||
date: newDate,
|
||||
time: time,
|
||||
includeTime: includeTime ?? false,
|
||||
);
|
||||
},
|
||||
(dateData) {
|
||||
var newDateData = dateData;
|
||||
if (date != null && !isSameDay(newDateData.date, date)) {
|
||||
newDateData = newDateData.copyWith(date: date);
|
||||
newDateData = newDateData.copyWith(
|
||||
date: _utcToLocalAddTime(date),
|
||||
);
|
||||
}
|
||||
|
||||
if (newDateData.time != time) {
|
||||
@@ -151,6 +159,21 @@ class DateCellCalendarBloc
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _utcToLocalAddTime(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
// the incoming date is Utc. this trick converts it into Local
|
||||
// and add the current time, though
|
||||
// the time may be overwritten by explicitly provided time string
|
||||
return DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
now.hour,
|
||||
now.minute,
|
||||
now.second,
|
||||
);
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
|
||||
switch (state.dateTypeOptionPB.timeFormat) {
|
||||
@@ -290,10 +313,7 @@ Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
|
||||
Option<DateCellData> dateData = none();
|
||||
if (cellData != null) {
|
||||
final timestamp = cellData.timestamp * 1000;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamp.toInt(),
|
||||
isUtc: true,
|
||||
);
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||
dateData = Some(
|
||||
DateCellData(
|
||||
date: date,
|
||||
|
||||
+7
-1
@@ -8,9 +8,13 @@ import '../../cell_builder.dart';
|
||||
|
||||
class GridTextCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
bool? autofocus;
|
||||
|
||||
GridTextCellStyle({
|
||||
this.placeholder,
|
||||
this.textStyle,
|
||||
this.autofocus,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +70,9 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
autofocus: widget.cellStyle?.autofocus ?? false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
|
||||
+86
-47
@@ -19,6 +19,8 @@ import 'url_cell_bloc.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
bool? autofocus;
|
||||
|
||||
List<GridURLCellAccessoryType> accessoryTypes;
|
||||
|
||||
@@ -29,8 +31,8 @@ class GridURLCellStyle extends GridCellStyle {
|
||||
}
|
||||
|
||||
enum GridURLCellAccessoryType {
|
||||
edit,
|
||||
copyURL,
|
||||
visitURL,
|
||||
}
|
||||
|
||||
class GridURLCell extends GridCellWidget {
|
||||
@@ -56,15 +58,14 @@ class GridURLCell extends GridCellWidget {
|
||||
GridCellAccessoryBuildContext buildContext,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
case GridURLCellAccessoryType.visitURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
builder: (Key key) => _EditURLAccessory(
|
||||
builder: (Key key) => _VisitURLAccessory(
|
||||
key: key,
|
||||
anchorContext: buildContext.anchorContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
);
|
||||
|
||||
case GridURLCellAccessoryType.copyURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
@@ -89,11 +90,11 @@ class GridURLCell extends GridCellWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL
|
||||
if (accessories.isEmpty) {
|
||||
accessories.add(
|
||||
accessoryFromType(
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
buildContext,
|
||||
),
|
||||
);
|
||||
@@ -106,6 +107,8 @@ class GridURLCell extends GridCellWidget {
|
||||
class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
final _popoverController = PopoverController();
|
||||
late URLCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -113,6 +116,8 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
widget.cellControllerBuilder.build() as URLCellController;
|
||||
_cellBloc = URLCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -122,60 +127,61 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
builder: (context, state) {
|
||||
final richText = Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
final urlEditor = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
);
|
||||
|
||||
return AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
offset: const Offset(0, 8),
|
||||
child: SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
onTap: () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: 1,
|
||||
style: (widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
autofocus: false,
|
||||
onEditingComplete: focusChanged,
|
||||
onSubmitted: (value) => focusChanged(isUrlSubmitted: true),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
bottom: GridSize.cellContentInsets.bottom,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return URLEditorPopover(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as URLCellController,
|
||||
onExit: () => _popoverController.close(),
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellEditing.value = false;
|
||||
},
|
||||
);
|
||||
return urlEditor;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void focusChanged({
|
||||
bool isUrlSubmitted = false,
|
||||
}) {
|
||||
if (mounted) {
|
||||
if (_cellBloc.isClosed == false &&
|
||||
_controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEvent.updateURL(_controller.text));
|
||||
}
|
||||
if (isUrlSubmitted) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
widget.onCellEditing.value = true;
|
||||
@@ -269,3 +275,36 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory>
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
}
|
||||
}
|
||||
|
||||
class _VisitURLAccessory extends StatefulWidget {
|
||||
final URLCellController cellContext;
|
||||
const _VisitURLAccessory({required this.cellContext, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VisitURLAccessoryState();
|
||||
}
|
||||
|
||||
class _VisitURLAccessoryState extends State<_VisitURLAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return svgWidget(
|
||||
"editor/link",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
var content =
|
||||
widget.cellContext.getCellData(loadIfNotExist: false)?.content ?? "";
|
||||
if (!content.contains('http://')) {
|
||||
content = 'http://$content';
|
||||
}
|
||||
final uri = Uri.parse(content);
|
||||
if (content.isNotEmpty) {
|
||||
canLaunchUrl(uri).then((value) => launchUrl(uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/grid/application/row/row_detail_bloc.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/image.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> {
|
||||
final padding = const EdgeInsets.symmetric(
|
||||
horizontal: 40,
|
||||
vertical: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
child: BlocProvider(
|
||||
create: (context) {
|
||||
final bloc = RowDetailBloc(
|
||||
dataController: widget.dataController,
|
||||
);
|
||||
bloc.add(const RowDetailEvent.initial());
|
||||
return bloc;
|
||||
return RowDetailBloc(dataController: widget.dataController)
|
||||
..add(const RowDetailEvent.initial());
|
||||
},
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
children: [
|
||||
const _Header(),
|
||||
Expanded(
|
||||
child: _PropertyColumn(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
viewId: widget.dataController.viewId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
// using ListView here for future expansion:
|
||||
// - header and cover image
|
||||
// - lower rich text area
|
||||
IntrinsicHeight(child: _responsiveRowInfo()),
|
||||
const Divider(height: 1.0),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
const _Header({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: Row(
|
||||
children: const [Spacer(), _CloseButton()],
|
||||
),
|
||||
Widget _responsiveRowInfo() {
|
||||
final rowDataColumn = _PropertyColumn(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
viewId: widget.dataController.viewId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
final rowOptionColumn = _RowOptionColumn(
|
||||
viewId: widget.dataController.viewId,
|
||||
rowId: widget.dataController.rowId,
|
||||
);
|
||||
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 _PropertyColumn extends StatelessWidget {
|
||||
final String viewId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
final ScrollController _scrollController;
|
||||
_PropertyColumn({
|
||||
const _PropertyColumn({
|
||||
required this.viewId,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : _scrollController = ScrollController(),
|
||||
super(key: key);
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -127,63 +130,61 @@ class _PropertyColumn extends StatelessWidget {
|
||||
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _wrapScrollbar(buildPropertyCells(state))),
|
||||
const VSpace(10),
|
||||
_CreatePropertyButton(
|
||||
viewId: viewId,
|
||||
onClosed: _scrollToNewProperty,
|
||||
_RowTitle(
|
||||
cellId: state.gridCells
|
||||
.firstWhereOrNull((e) => e.fieldInfo.isPrimary),
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
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) {
|
||||
return ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemCount: state.gridCells.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _PropertyCell(
|
||||
cellId: state.gridCells[index],
|
||||
cellBuilder: cellBuilder,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const VSpace(2);
|
||||
},
|
||||
class _RowTitle extends StatelessWidget {
|
||||
final CellIdentifier? cellId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const _RowTitle({this.cellId, required this.cellBuilder, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (cellId == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final style = GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
autofocus: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapScrollbar(Widget child) {
|
||||
return ScrollbarListStack(
|
||||
axis: Axis.vertical,
|
||||
controller: _scrollController,
|
||||
barSize: GridSize.scrollBarSize,
|
||||
autoHideScrollbar: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _scrollToNewProperty() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
});
|
||||
return cellBuilder.build(cellId!, style: style);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreatePropertyButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
final VoidCallback onClosed;
|
||||
|
||||
const _CreatePropertyButton({
|
||||
required this.viewId,
|
||||
required this.onClosed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -206,10 +207,9 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
onClose: widget.onClosed,
|
||||
child: Container(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
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 {
|
||||
@@ -369,10 +361,76 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,9 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
||||
if (temporaryNodeTypes.contains(node.type)) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
if (kCoverType == node.type && !node.path.equals([0])) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
}
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
await editorState.apply(transaction, withUpdateCursor: false);
|
||||
|
||||
+12
-3
@@ -1,4 +1,5 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -24,6 +25,8 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor;
|
||||
final foregroundColor = Theme.of(context).colorScheme.onBackground;
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
@@ -43,10 +46,16 @@ class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
||||
onPressed: (int index) {
|
||||
_updateSelectedFontSize(_fontSizes[index].item2);
|
||||
},
|
||||
color: foregroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
selectedColor: Theme.of(context).colorScheme.tertiary,
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).hintColor,
|
||||
borderColor: foregroundColor,
|
||||
borderWidth: 0.5,
|
||||
// when selected
|
||||
selectedColor: foregroundColor,
|
||||
selectedBorderColor: foregroundColor,
|
||||
fillColor: selectedBgColor,
|
||||
// when hover
|
||||
hoverColor: selectedBgColor.withOpacity(0.3),
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 80.0,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -12,7 +14,9 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<int>(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
offset: const Offset(0, 30),
|
||||
tooltip: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
|
||||
+62
-35
@@ -393,21 +393,32 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AppFlowyPopover(
|
||||
onClose: () {
|
||||
setOverlayButtonsHidden(true);
|
||||
},
|
||||
offset: const Offset(-125, 10),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
constraints: BoxConstraints.loose(const Size(380, 450)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: RoundedTextButton(
|
||||
onPressed: () {
|
||||
popoverController.show();
|
||||
},
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
textColor: Theme.of(context).colorScheme.tertiary,
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
|
||||
width: 120,
|
||||
height: 28,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
child: Visibility(
|
||||
maintainState: true,
|
||||
maintainAnimation: true,
|
||||
maintainSize: true,
|
||||
visible: !isOverlayButtonsHidden,
|
||||
child: RoundedTextButton(
|
||||
onPressed: () {
|
||||
popoverController.show();
|
||||
setOverlayButtonsHidden(true);
|
||||
},
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
textColor: Theme.of(context).colorScheme.tertiary,
|
||||
fillColor:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
width: 120,
|
||||
height: 28,
|
||||
title: LocaleKeys.document_plugins_cover_changeCover.tr(),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return ChangeCoverPopover(
|
||||
@@ -418,18 +429,24 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
FlowyIconButton(
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.8),
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
width: 28,
|
||||
icon: svgWidget(
|
||||
'editor/delete',
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
Visibility(
|
||||
maintainAnimation: true,
|
||||
maintainSize: true,
|
||||
maintainState: true,
|
||||
visible: !isOverlayButtonsHidden,
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Theme.of(context).colorScheme.surface,
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
width: 28,
|
||||
icon: svgWidget(
|
||||
'editor/delete',
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onCoverChanged(CoverSelectionType.initial, null);
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onCoverChanged(CoverSelectionType.initial, null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -477,20 +494,30 @@ class _CoverImageState extends State<_CoverImage> {
|
||||
break;
|
||||
}
|
||||
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an error
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: OverflowBox(
|
||||
maxWidth: screenSize.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
child: coverImage,
|
||||
),
|
||||
hasCover ? _buildCoverOverlayButtons(context) : const SizedBox()
|
||||
],
|
||||
return MouseRegion(
|
||||
onEnter: (event) {
|
||||
setOverlayButtonsHidden(false);
|
||||
},
|
||||
onExit: (event) {
|
||||
setOverlayButtonsHidden(true);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: OverflowBox(
|
||||
maxWidth: screenSize.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
child: coverImage,
|
||||
),
|
||||
hasCover
|
||||
? _buildCoverOverlayButtons(context)
|
||||
: const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+45
-50
@@ -1,11 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.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' hide FlowySvg;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiPopover extends StatefulWidget {
|
||||
@@ -33,57 +32,53 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
customWidget: (Config config, EmojiViewState state) {
|
||||
return Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
|
||||
child: DefaultEmojiPickerView(config, state),
|
||||
),
|
||||
_buildDeleteButtonIfNeed(),
|
||||
],
|
||||
);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButtonIfNeed() {
|
||||
if (!widget.showRemoveButton) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return FlowyButton(
|
||||
onTap: () => widget.removeIcon(),
|
||||
useIntrinsicWidth: true,
|
||||
text: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
child: Column(
|
||||
children: [
|
||||
const FlowySvg(name: 'editor/delete'),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
if (widget.showRemoveButton)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: DeleteButton(onPressed: widget.removeIcon),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
const DeleteButton({required this.onPressed, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: onPressed,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(name: 'editor/delete'),
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
-19
@@ -29,29 +29,32 @@ void _showEmojiSelectionMenu(
|
||||
menuService.dismiss();
|
||||
|
||||
_emojiSelectionMenu?.remove();
|
||||
_emojiSelectionMenu = OverlayEntry(builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
child: EmojiSelectionMenu(
|
||||
editorState: editorState,
|
||||
onSubmitted: (text) {
|
||||
// insert emoji
|
||||
editorState.insertEmoji(text);
|
||||
},
|
||||
onExit: () {
|
||||
_dismissEmojiSelectionMenu();
|
||||
//close emoji panel
|
||||
},
|
||||
_emojiSelectionMenu = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
child: EmojiSelectionMenu(
|
||||
editorState: editorState,
|
||||
onSubmitted: (text) {
|
||||
// insert emoji
|
||||
editorState.insertEmoji(text);
|
||||
},
|
||||
onExit: () {
|
||||
_dismissEmojiSelectionMenu();
|
||||
//close emoji panel
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},);
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_emojiSelectionMenu!);
|
||||
|
||||
_editorState = editorState;
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissEmojiSelectionMenu);
|
||||
}
|
||||
@@ -62,6 +65,7 @@ void _dismissEmojiSelectionMenu() {
|
||||
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissEmojiSelectionMenu);
|
||||
_editorState?.service.keyboardService?.enable();
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -270,7 +270,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ abstract class OpenAIRepository {
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
});
|
||||
|
||||
/// Get edits from GPT-3
|
||||
|
||||
+19
-1
@@ -5,7 +5,9 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
enum SmartEditAction {
|
||||
summarize,
|
||||
fixSpelling;
|
||||
fixSpelling,
|
||||
improveWriting,
|
||||
makeItLonger;
|
||||
|
||||
String get toInstruction {
|
||||
switch (this) {
|
||||
@@ -13,6 +15,10 @@ enum SmartEditAction {
|
||||
return 'Tl;dr';
|
||||
case SmartEditAction.fixSpelling:
|
||||
return 'Correct this to standard English:';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this in your own words:';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +28,10 @@ enum SmartEditAction {
|
||||
return '$input\n\nTl;dr';
|
||||
case SmartEditAction.fixSpelling:
|
||||
return 'Correct this to standard English:\n\n$input';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this:\n\n$input';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:\n\n$input';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +41,10 @@ enum SmartEditAction {
|
||||
return SmartEditAction.summarize;
|
||||
case 1:
|
||||
return SmartEditAction.fixSpelling;
|
||||
case 2:
|
||||
return SmartEditAction.improveWriting;
|
||||
case 3:
|
||||
return SmartEditAction.makeItLonger;
|
||||
}
|
||||
return SmartEditAction.fixSpelling;
|
||||
}
|
||||
@@ -41,6 +55,10 @@ enum SmartEditAction {
|
||||
return LocaleKeys.document_plugins_smartEditSummarize.tr();
|
||||
case SmartEditAction.fixSpelling:
|
||||
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
|
||||
case SmartEditAction.improveWriting:
|
||||
return LocaleKeys.document_plugins_smartEditImproveWriting.tr();
|
||||
case SmartEditAction.makeItLonger:
|
||||
return LocaleKeys.document_plugins_smartEditMakeLonger.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-51
@@ -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/widgets/discard_dialog.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_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -242,7 +242,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
),
|
||||
onPressed: () async {
|
||||
await _onReplace();
|
||||
_onExit();
|
||||
await _onExit();
|
||||
},
|
||||
),
|
||||
const Space(10, 0),
|
||||
@@ -257,7 +257,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
),
|
||||
onPressed: () async {
|
||||
await _onInsertBelow();
|
||||
_onExit();
|
||||
await _onExit();
|
||||
},
|
||||
),
|
||||
const Space(10, 0),
|
||||
@@ -272,10 +272,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
),
|
||||
onPressed: () async => await _onExit(),
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
const Spacer(flex: 2),
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -298,7 +301,22 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
selection,
|
||||
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 {
|
||||
@@ -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 {
|
||||
@@ -333,49 +360,42 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
}
|
||||
|
||||
Future<void> _requestCompletions() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold((l) async {
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: client,
|
||||
apiKey: l.openaiKey,
|
||||
);
|
||||
final openAIRepository = await getIt.getAsync<OpenAIRepository>();
|
||||
|
||||
var lines = input.split('\n\n');
|
||||
if (action == SmartEditAction.summarize) {
|
||||
lines = [lines.join('\n')];
|
||||
}
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
final element = lines[i];
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
useAction: true,
|
||||
prompt: action.prompt(element),
|
||||
onStart: () async {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
},
|
||||
onProcess: (response) async {
|
||||
setState(() {
|
||||
this.result += response.choices.first.text;
|
||||
});
|
||||
},
|
||||
onEnd: () async {
|
||||
setState(() {
|
||||
if (i != lines.length - 1) {
|
||||
this.result += '\n';
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
await _showError(error.message);
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
}
|
||||
}, (r) async {
|
||||
await _showError(r.msg);
|
||||
await _onExit();
|
||||
});
|
||||
var lines = input.split('\n\n');
|
||||
if (action == SmartEditAction.summarize) {
|
||||
lines = [lines.join('\n')];
|
||||
}
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
final element = lines[i];
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
useAction: true,
|
||||
prompt: action.prompt(element),
|
||||
onStart: () async {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
},
|
||||
onProcess: (response) async {
|
||||
setState(() {
|
||||
if (response.choices.first.text != '\n') {
|
||||
result += response.choices.first.text;
|
||||
}
|
||||
});
|
||||
},
|
||||
onEnd: () async {
|
||||
setState(() {
|
||||
if (i != lines.length - 1) {
|
||||
result += '\n';
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
await _showError(error.message);
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showError(String message) async {
|
||||
|
||||
@@ -4,10 +4,14 @@ class TrashSizes {
|
||||
static double get fileNameWidth => 320 * scale;
|
||||
static double get lashModifyWidth => 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 =>
|
||||
TrashSizes.fileNameWidth +
|
||||
TrashSizes.lashModifyWidth +
|
||||
TrashSizes.createTimeWidth +
|
||||
TrashSizes.padding;
|
||||
TrashSizes.padding +
|
||||
// restore and delete icon
|
||||
2 * TrashSizes.actionIconWidth;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -38,23 +36,19 @@ class TrashCell extends StatelessWidget {
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyIconButton(
|
||||
width: 26,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
width: TrashSizes.actionIconWidth,
|
||||
onPressed: onRestore,
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
icon: svgWidget(
|
||||
"editor/restore",
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
icon: const FlowySvg(name: 'editor/restore'),
|
||||
),
|
||||
const HSpace(20),
|
||||
FlowyIconButton(
|
||||
width: 26,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
width: TrashSizes.actionIconWidth,
|
||||
onPressed: onDelete,
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
icon: svgWidget(
|
||||
"editor/delete",
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
icon: const FlowySvg(name: 'editor/delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -96,10 +96,7 @@ class _TrashPageState extends State<TrashPage> {
|
||||
IntrinsicWidth(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()),
|
||||
leftIcon: svgWidget(
|
||||
'editor/restore',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
leftIcon: const FlowySvg(name: 'editor/restore'),
|
||||
onTap: () => context.read<TrashBloc>().add(
|
||||
const TrashEvent.restoreAll(),
|
||||
),
|
||||
@@ -109,10 +106,7 @@ class _TrashPageState extends State<TrashPage> {
|
||||
IntrinsicWidth(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()),
|
||||
leftIcon: svgWidget(
|
||||
'editor/delete',
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
leftIcon: const FlowySvg(name: 'editor/delete'),
|
||||
onTap: () =>
|
||||
context.read<TrashBloc>().add(const TrashEvent.deleteAll()),
|
||||
),
|
||||
|
||||
@@ -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/setting/property_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_service.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_impl.dart';
|
||||
@@ -27,6 +28,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class DependencyResolver {
|
||||
static Future<void> resolve(GetIt getIt) async {
|
||||
@@ -44,8 +46,25 @@ class DependencyResolver {
|
||||
}
|
||||
}
|
||||
|
||||
void _resolveCommonService(GetIt getIt) {
|
||||
void _resolveCommonService(GetIt getIt) async {
|
||||
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) {
|
||||
|
||||
@@ -28,6 +28,7 @@ class InitAppWidgetTask extends LaunchTask {
|
||||
EasyLocalization(
|
||||
supportedLocales: const [
|
||||
// In alphabetical order
|
||||
Locale('ar', 'AR'),
|
||||
Locale('ca', 'ES'),
|
||||
Locale('de', 'DE'),
|
||||
Locale('en'),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class ColorGenerator {
|
||||
Color generateColorFromString(String string) {
|
||||
final hash = string.hashCode;
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
return Color.fromRGBO(r, g, b, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -242,8 +242,15 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
||||
shadow: theme.shadow,
|
||||
);
|
||||
|
||||
const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
|
||||
MaterialState.pressed,
|
||||
MaterialState.hovered,
|
||||
MaterialState.dragged,
|
||||
};
|
||||
|
||||
return ThemeData(
|
||||
brightness: brightness,
|
||||
dialogBackgroundColor: theme.surface,
|
||||
textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: theme.main2,
|
||||
@@ -264,20 +271,20 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
||||
contentTextStyle: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
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) {
|
||||
const Set<MaterialState> interactiveStates = <MaterialState>{
|
||||
MaterialState.pressed,
|
||||
MaterialState.hovered,
|
||||
MaterialState.dragged,
|
||||
};
|
||||
if (states.any(interactiveStates.contains)) {
|
||||
return 5.0;
|
||||
if (states.any(scrollbarInteractiveStates.contains)) {
|
||||
return 4;
|
||||
}
|
||||
return 3.0;
|
||||
}),
|
||||
crossAxisMargin: 0.0,
|
||||
mainAxisMargin: 0.0,
|
||||
mainAxisMargin: 6.0,
|
||||
radius: Corners.s10Radius,
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
@@ -310,7 +317,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
||||
greySelect: theme.bg3,
|
||||
lightGreyHover: theme.hoverBG3,
|
||||
toggleOffFill: theme.shader5,
|
||||
progressBarBGcolor: theme.progressBarBGcolor,
|
||||
progressBarBGColor: theme.progressBarBGColor,
|
||||
toggleButtonBGColor: theme.toggleButtonBGColor,
|
||||
code: _getFontStyle(
|
||||
fontFamily: monospaceFontFamily,
|
||||
fontColor: theme.shader3,
|
||||
|
||||
+15
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsFileExportState {
|
||||
@@ -8,6 +9,20 @@ class SettingsFileExportState {
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<ViewPB> get selectedViews {
|
||||
final selectedViews = <ViewPB>[];
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (selectedApps[i]) {
|
||||
for (var j = 0; j < apps[i].belongings.items.length; j++) {
|
||||
if (selectedItems[i][j]) {
|
||||
selectedViews.add(apps[i].belongings.items[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedViews;
|
||||
}
|
||||
|
||||
List<AppPB> apps;
|
||||
List<bool> expanded = [];
|
||||
List<bool> selectedApps = [];
|
||||
|
||||
+2
@@ -25,6 +25,8 @@ class SettingsLocation {
|
||||
if (Platform.isMacOS) {
|
||||
// remove the prefix `/Volumes/*`
|
||||
return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), '');
|
||||
} else if (Platform.isWindows) {
|
||||
return _path?.replaceAll("/", "\\");
|
||||
}
|
||||
return _path;
|
||||
}
|
||||
|
||||
+4
@@ -61,6 +61,10 @@ class ViewSection extends StatelessWidget {
|
||||
.add(ViewSectionEvent.moveView(oldIndex, index));
|
||||
},
|
||||
ignorePrimaryScrollController: true,
|
||||
buildDraggableFeedback: (context, constraints, child) => ConstrainedBox(
|
||||
constraints: constraints,
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ class HomeMenu extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
Material(color: Colors.transparent, child: child),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
@@ -45,8 +46,31 @@ class MenuUser extends StatelessWidget {
|
||||
String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
|
||||
if (iconUrl.isEmpty) {
|
||||
iconUrl = defaultUserAvatar;
|
||||
final String name = context.read<MenuUserBloc>().state.userProfile.name;
|
||||
final Color color = ColorGenerator().generateColorFromString(name);
|
||||
const initialsCount = 2;
|
||||
// Taking the first letters of the name components and limiting to 2 elements
|
||||
final nameInitials = name
|
||||
.split(' ')
|
||||
.where((element) => element.isNotEmpty)
|
||||
.take(initialsCount)
|
||||
.map((element) => element[0].toUpperCase())
|
||||
.join('');
|
||||
return Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
nameInitials,
|
||||
color: Colors.white,
|
||||
fontSize: nameInitials.length == initialsCount ? 12 : 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
|
||||
+4
-4
@@ -77,8 +77,8 @@ class ThemeSetting extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(theme),
|
||||
rightIcon: currentTheme == theme
|
||||
? svgWidget("grid/checkmark")
|
||||
: const SizedBox(),
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentTheme != theme) {
|
||||
context.read<AppearanceSettingsCubit>().setTheme(theme);
|
||||
@@ -134,8 +134,8 @@ class ThemeModeSetting extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(_themeModeLabelText(themeMode)),
|
||||
rightIcon: currentThemeMode == themeMode
|
||||
? svgWidget("grid/checkmark")
|
||||
: const SizedBox(),
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentThemeMode != themeMode) {
|
||||
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class SettingsExportFileWidget extends StatefulWidget {
|
||||
const SettingsExportFileWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsExportFileWidget> createState() =>
|
||||
SettingsExportFileWidgetState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_files_exportData.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: LocaleKeys.settings_files_open.tr(),
|
||||
child: FlowyIconButton(
|
||||
height: 40,
|
||||
width: 40,
|
||||
icon: const Icon(Icons.folder_open_outlined),
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const FlowyDialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
child: FileExporterWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -12,8 +12,8 @@ import '../../../../startup/launch_configuration.dart';
|
||||
import '../../../../startup/startup.dart';
|
||||
import '../../../../startup/tasks/prelude.dart';
|
||||
|
||||
class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
const SettingsFileLocationCustomzier({
|
||||
class SettingsFileLocationCustomizer extends StatefulWidget {
|
||||
const SettingsFileLocationCustomizer({
|
||||
super.key,
|
||||
required this.cubit,
|
||||
});
|
||||
@@ -21,13 +21,13 @@ class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
final SettingsLocationCubit cubit;
|
||||
|
||||
@override
|
||||
State<SettingsFileLocationCustomzier> createState() =>
|
||||
SettingsFileLocationCustomzierState();
|
||||
State<SettingsFileLocationCustomizer> createState() =>
|
||||
SettingsFileLocationCustomizerState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsFileLocationCustomzierState
|
||||
extends State<SettingsFileLocationCustomzier> {
|
||||
class SettingsFileLocationCustomizerState
|
||||
extends State<SettingsFileLocationCustomizer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<SettingsLocationCubit>.value(
|
||||
@@ -35,9 +35,8 @@ class SettingsFileLocationCustomzierState
|
||||
child: BlocBuilder<SettingsLocationCubit, SettingsLocation>(
|
||||
builder: (context, state) {
|
||||
return ListTile(
|
||||
title: FlowyText.regular(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_files_defaultLocation.tr(),
|
||||
fontSize: 15.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Tooltip(
|
||||
@@ -63,7 +62,6 @@ class SettingsFileLocationCustomzierState
|
||||
},
|
||||
child: FlowyText.regular(
|
||||
state.path ?? '',
|
||||
fontSize: 10.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -74,7 +72,11 @@ class SettingsFileLocationCustomzierState
|
||||
Tooltip(
|
||||
message: LocaleKeys.settings_files_restoreLocation.tr(),
|
||||
child: FlowyIconButton(
|
||||
height: 40,
|
||||
width: 40,
|
||||
icon: const Icon(Icons.restore_outlined),
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
final result = await appFlowyDocumentDirectory();
|
||||
await _setCustomLocation(result.path);
|
||||
@@ -96,7 +98,11 @@ class SettingsFileLocationCustomzierState
|
||||
Tooltip(
|
||||
message: LocaleKeys.settings_files_customizeLocation.tr(),
|
||||
child: FlowyIconButton(
|
||||
height: 40,
|
||||
width: 40,
|
||||
icon: const Icon(Icons.folder_open_outlined),
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
final result =
|
||||
await getIt<FilePickerService>().getDirectoryPath();
|
||||
|
||||
+126
-38
@@ -1,15 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class FileExporterWidget extends StatefulWidget {
|
||||
@@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget {
|
||||
class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
// Map<String, List<String>> _selectedPages = {};
|
||||
|
||||
SettingsFileExporterCubit? cubit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(child: _buildFileSelector(context)),
|
||||
const VSpace(8),
|
||||
_buildButtons(context)
|
||||
],
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
cubit ??= SettingsFileExporterCubit(apps: apps);
|
||||
return BlocProvider<SettingsFileExporterCubit>.value(
|
||||
value: cubit!,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Expanded(child: _ExpandedList()),
|
||||
const VSpace(8),
|
||||
_buildButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildButtons(BuildContext context) {
|
||||
Widget _buildButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
@@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
onPressed: () async {
|
||||
await getIt<FilePickerService>()
|
||||
.getDirectoryPath()
|
||||
.then((exportPath) {
|
||||
Navigator.of(context).pop();
|
||||
.then((exportPath) async {
|
||||
if (exportPath != null && cubit != null) {
|
||||
final views = cubit!.state.selectedViews;
|
||||
final result =
|
||||
await _AppFlowyFileExporter.exportToPath(exportPath, views);
|
||||
if (result.item1) {
|
||||
// success
|
||||
_showToast(LocaleKeys.settings_files_exportFileSuccess.tr());
|
||||
} else {
|
||||
_showToast(
|
||||
LocaleKeys.settings_files_exportFileFail.tr() +
|
||||
result.item2.join('\n'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showToast(LocaleKeys.settings_files_exportFileFail.tr());
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).popUntil(
|
||||
(router) => router.settings.name == '/',
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
|
||||
_buildFileSelector(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
return BlocProvider<SettingsFileExporterCubit>(
|
||||
create: (_) => SettingsFileExporterCubit(apps: apps),
|
||||
child: const _ExpandedList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
void _showToast(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText(
|
||||
message,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
final apps = state.apps;
|
||||
final expanded = state.expanded;
|
||||
final selectedItems = state.selectedItems;
|
||||
final isExpaned = expanded[index] == true;
|
||||
final isExpanded = expanded[index] == true;
|
||||
List<Widget> expandedChildren = [];
|
||||
if (isExpaned) {
|
||||
if (isExpanded) {
|
||||
for (var i = 0; i < selectedItems[index].length; i++) {
|
||||
final name = apps[index].belongings.items[i].name;
|
||||
final checkbox = CheckboxListTile(
|
||||
@@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
child: ListTile(
|
||||
title: FlowyText.medium(apps[index].name),
|
||||
trailing: Icon(
|
||||
isExpaned
|
||||
isExpanded
|
||||
? Icons.arrow_drop_down_rounded
|
||||
: Icons.arrow_drop_up_rounded,
|
||||
),
|
||||
@@ -182,3 +219,54 @@ extension AppFlowy on dartz.Either {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyFileExporter {
|
||||
static Future<Tuple2<bool, List<String>>> exportToPath(
|
||||
String path,
|
||||
List<ViewPB> views,
|
||||
) async {
|
||||
final failedFileNames = <String>[];
|
||||
final Map<String, int> names = {};
|
||||
final documentService = DocumentService();
|
||||
for (final view in views) {
|
||||
String? content;
|
||||
String? fileExtension;
|
||||
switch (view.layout) {
|
||||
case ViewLayoutTypePB.Document:
|
||||
final document = await documentService.openDocument(view: view);
|
||||
document.fold(
|
||||
(l) => content = l.content,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdoc';
|
||||
break;
|
||||
default:
|
||||
final result = await exportDatabase(view.id);
|
||||
result.fold(
|
||||
(pb) => content = pb.data,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdb';
|
||||
break;
|
||||
}
|
||||
if (content != null) {
|
||||
final count = names.putIfAbsent(view.name, () => 0);
|
||||
final name = count == 0 ? view.name : '${view.name}($count)';
|
||||
final file = File(p.join(path, '$name.$fileExtension'));
|
||||
await file.writeAsString(content!);
|
||||
names[view.name] = count + 1;
|
||||
} else {
|
||||
failedFileNames.add(view.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple2(failedFileNames.isEmpty, failedFileNames);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dartz.Either<ExportCSVPB, FlowyError>> exportDatabase(
|
||||
String viewId,
|
||||
) async {
|
||||
final payload = DatabaseViewIdPB.create()..value = viewId;
|
||||
return DatabaseEventExportCSV(payload).send();
|
||||
}
|
||||
+10
-11
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,22 +15,20 @@ class SettingsFileSystemView extends StatefulWidget {
|
||||
|
||||
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
|
||||
final _locationCubit = SettingsLocationCubit()..fetchLocation();
|
||||
late final _items = [
|
||||
SettingsFileLocationCustomizer(
|
||||
cubit: _locationCubit,
|
||||
),
|
||||
const SettingsExportFileWidget()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return SettingsFileLocationCustomzier(
|
||||
cubit: _locationCubit,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
// return _buildExportDatabaseButton();
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => _items[index],
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemCount: 2, // make the divider taking effect.
|
||||
itemCount: _items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
+89
-71
@@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/language.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -12,85 +14,101 @@ class SettingsLanguageView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.settings_menu_language.tr()),
|
||||
const LanguageSelectorDropdown(),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_language.tr(),
|
||||
),
|
||||
),
|
||||
LanguageSelector(currentLocale: state.locale),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageSelectorDropdown extends StatefulWidget {
|
||||
const LanguageSelectorDropdown({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LanguageSelectorDropdown> createState() =>
|
||||
_LanguageSelectorDropdownState();
|
||||
}
|
||||
|
||||
class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
|
||||
Color currHoverColor = Colors.white.withOpacity(0.0);
|
||||
void hoverExitLanguage() {
|
||||
setState(() {
|
||||
currHoverColor = Colors.white.withOpacity(0.0);
|
||||
});
|
||||
}
|
||||
|
||||
void hoverEnterLanguage() {
|
||||
setState(() {
|
||||
currHoverColor = Theme.of(context).colorScheme.secondaryContainer;
|
||||
});
|
||||
}
|
||||
class LanguageSelector extends StatelessWidget {
|
||||
final Locale currentLocale;
|
||||
const LanguageSelector({
|
||||
super.key,
|
||||
required this.currentLocale,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => hoverEnterLanguage(),
|
||||
onExit: (_) => hoverExitLanguage(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: currHoverColor,
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
languageFromLocale(currentLocale),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
final allLocales = EasyLocalization.of(context)!.supportedLocales;
|
||||
|
||||
return LanguageItemsListView(
|
||||
allLocales: allLocales,
|
||||
currentLocale: currentLocale,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItemsListView extends StatelessWidget {
|
||||
const LanguageItemsListView({
|
||||
super.key,
|
||||
required this.allLocales,
|
||||
required this.currentLocale,
|
||||
});
|
||||
|
||||
final List<Locale> allLocales;
|
||||
final Locale currentLocale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final locale = allLocales[index];
|
||||
return LanguageItem(locale: locale, currentLocale: currentLocale);
|
||||
},
|
||||
itemCount: allLocales.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItem extends StatelessWidget {
|
||||
final Locale locale;
|
||||
final Locale currentLocale;
|
||||
const LanguageItem({
|
||||
super.key,
|
||||
required this.locale,
|
||||
required this.currentLocale,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
languageFromLocale(locale),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: DropdownButton<Locale>(
|
||||
value: context.locale,
|
||||
dropdownColor: Theme.of(context).cardColor,
|
||||
onChanged: (locale) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLocale(context, locale!);
|
||||
},
|
||||
autofocus: true,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
items:
|
||||
EasyLocalization.of(context)!.supportedLocales.map((locale) {
|
||||
return DropdownMenuItem<Locale>(
|
||||
value: locale,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: FlowyText.medium(
|
||||
languageFromLocale(locale),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
rightIcon: currentLocale == locale
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentLocale != locale) {
|
||||
context.read<AppearanceSettingsCubit>().setLocale(context, locale);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+121
-71
@@ -1,19 +1,20 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
const defaultUserAvatar = '1F600';
|
||||
const _iconSize = Size(60, 60);
|
||||
|
||||
class SettingsUserView extends StatelessWidget {
|
||||
final UserProfilePB user;
|
||||
@@ -63,27 +64,67 @@ class SettingsUserView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class UserNameInput extends StatelessWidget {
|
||||
class UserNameInput extends StatefulWidget {
|
||||
final String name;
|
||||
|
||||
const UserNameInput(
|
||||
this.name, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
UserNameInputState createState() => UserNameInputState();
|
||||
}
|
||||
|
||||
class UserNameInputState extends State<UserNameInput> {
|
||||
late TextEditingController _controller;
|
||||
|
||||
Timer? _debounce;
|
||||
final Duration _debounceDuration = const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.name);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: TextEditingController()..text = name,
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: LocaleKeys.settings_user_name.tr(),
|
||||
labelStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.onBackground),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
onSubmitted: (val) {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(val));
|
||||
onChanged: (val) {
|
||||
if (_debounce?.isActive ?? false) {
|
||||
_debounce!.cancel();
|
||||
}
|
||||
|
||||
_debounce = Timer(_debounceDuration, () {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserName(val));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _OpenaiKeyInput extends StatefulWidget {
|
||||
@@ -115,14 +156,26 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
controller: textEditingController,
|
||||
obscureText: !visible,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.onBackground),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
labelText: 'OpenAI Key',
|
||||
labelStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.w500),
|
||||
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
|
||||
suffixIcon: IconButton(
|
||||
iconSize: 15.0,
|
||||
icon: Icon(visible ? Icons.visibility : Icons.visibility_off),
|
||||
padding: EdgeInsets.zero,
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
suffixIcon: FlowyIconButton(
|
||||
width: 40,
|
||||
height: 40,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
icon: Icon(
|
||||
visible ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
visible = !visible;
|
||||
@@ -160,50 +213,48 @@ class _CurrentIcon extends StatelessWidget {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_user_selectAnIcon.tr(),
|
||||
fontSize: FontSizes.s16,
|
||||
),
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: IconGallery(setIcon),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
LocaleKeys.settings_user_icon.tr(),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.settings_user_icon.tr(),
|
||||
style: Theme.of(context).textTheme.titleSmall!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(5.0),
|
||||
decoration:
|
||||
BoxDecoration(border: Border.all(color: Colors.grey)),
|
||||
child: svgWidget('emoji/$iconUrl', size: const Size(60, 60)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: Corners.s6Border,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_user_selectAnIcon.tr(),
|
||||
fontSize: FontSizes.s16,
|
||||
),
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: IconGallery(setIcon),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(0, 5, 5, 5),
|
||||
child: svgWidget(
|
||||
'emoji/$iconUrl',
|
||||
size: _iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -261,14 +312,13 @@ class IconOption extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setIcon(iconUrl);
|
||||
},
|
||||
child: svgWidget('emoji/$iconUrl'),
|
||||
),
|
||||
return InkWell(
|
||||
borderRadius: Corners.s6Border,
|
||||
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
onTap: () {
|
||||
setIcon(iconUrl);
|
||||
},
|
||||
child: svgWidget('emoji/$iconUrl', size: _iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -295,7 +295,7 @@ class DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
emoji.emoji,
|
||||
textScaleFactor: 1.0,
|
||||
|
||||
+11
-2
@@ -73,6 +73,11 @@ class BubbleActionList extends StatelessWidget {
|
||||
"https://appflowy.gitbook.io/docs/essential-documentation/shortcuts",
|
||||
);
|
||||
break;
|
||||
case BubbleAction.markdown:
|
||||
_launchURL(
|
||||
"https://appflowy.gitbook.io/docs/essential-documentation/markdown",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +174,7 @@ class FlowyVersionDescription extends CustomActionCell {
|
||||
}
|
||||
}
|
||||
|
||||
enum BubbleAction { whatsNews, help, debug, shortcuts }
|
||||
enum BubbleAction { whatsNews, help, debug, shortcuts, markdown }
|
||||
|
||||
class BubbleActionWrapper extends ActionCell {
|
||||
final BubbleAction inner;
|
||||
@@ -193,19 +198,23 @@ extension QuestionBubbleExtension on BubbleAction {
|
||||
return LocaleKeys.questionBubble_debug_name.tr();
|
||||
case BubbleAction.shortcuts:
|
||||
return LocaleKeys.questionBubble_shortcuts.tr();
|
||||
case BubbleAction.markdown:
|
||||
return LocaleKeys.questionBubble_markdown.tr();
|
||||
}
|
||||
}
|
||||
|
||||
String get emoji {
|
||||
switch (this) {
|
||||
case BubbleAction.whatsNews:
|
||||
return '⭐️';
|
||||
return '🆕';
|
||||
case BubbleAction.help:
|
||||
return '👥';
|
||||
case BubbleAction.debug:
|
||||
return '🐛';
|
||||
case BubbleAction.shortcuts:
|
||||
return '📋';
|
||||
case BubbleAction.markdown:
|
||||
return '✨';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,6 +588,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.macos;
|
||||
PRODUCT_NAME = AppFlowy;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -73,9 +73,10 @@ abstract class FlowyColorScheme {
|
||||
//the text color when it is hovered
|
||||
final Color hoverFG;
|
||||
final Color questionBubbleBG;
|
||||
final Color progressBarBGcolor;
|
||||
final Color progressBarBGColor;
|
||||
//editor toolbar BG color
|
||||
final Color toolbarColor;
|
||||
final Color toggleButtonBGColor;
|
||||
|
||||
const FlowyColorScheme({
|
||||
required this.surface,
|
||||
@@ -121,8 +122,9 @@ abstract class FlowyColorScheme {
|
||||
required this.hoverBG3,
|
||||
required this.hoverFG,
|
||||
required this.questionBubbleBG,
|
||||
required this.progressBarBGcolor,
|
||||
required this.progressBarBGColor,
|
||||
required this.toolbarColor,
|
||||
required this.toggleButtonBGColor,
|
||||
});
|
||||
|
||||
factory FlowyColorScheme.builtIn(String themeName, Brightness brightness) {
|
||||
|
||||
@@ -10,6 +10,7 @@ const _lightBg1 = Color(0xFFFFD13E);
|
||||
const _lightBg2 = Color(0xffedeef2);
|
||||
const _lightShader1 = Color(0xff333333);
|
||||
const _lightShader3 = Color(0xff828282);
|
||||
const _lightShader5 = Color(0xffe0e0e0);
|
||||
const _lightShader6 = Color(0xfff2f2f2);
|
||||
const _lightMain1 = Color(0xffe21f74);
|
||||
const _lightTint9 = Color(0xffe1fbff);
|
||||
@@ -35,7 +36,7 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
shader2: const Color(0xff4f4f4f),
|
||||
shader3: const Color(0xff828282),
|
||||
shader4: const Color(0xffbdbdbd),
|
||||
shader5: const Color(0xffe0e0e0),
|
||||
shader5: _lightShader5,
|
||||
shader6: const Color(0xfff2f2f2),
|
||||
shader7: _black,
|
||||
bg1: _lightBg1,
|
||||
@@ -68,55 +69,58 @@ class DandelionColorScheme extends FlowyColorScheme {
|
||||
hoverBG3: _lightShader6,
|
||||
hoverFG: _lightShader1,
|
||||
questionBubbleBG: _lightSelector,
|
||||
progressBarBGcolor: _lightTint9,
|
||||
progressBarBGColor: _lightTint9,
|
||||
toolbarColor: _lightShader1,
|
||||
toggleButtonBGColor: _lightShader5,
|
||||
);
|
||||
|
||||
const DandelionColorScheme.dark()
|
||||
: super(
|
||||
surface: const Color(0xff292929),
|
||||
hover: const Color(0xff1f1f1f),
|
||||
selector: const Color(0xff333333),
|
||||
red: const Color(0xfffb006d),
|
||||
yellow: const Color(0xffffd667),
|
||||
green: const Color(0xff66cf80),
|
||||
shader1: _white,
|
||||
shader2: _darkShader2,
|
||||
shader3: const Color(0xff828282),
|
||||
shader4: const Color(0xffbdbdbd),
|
||||
shader5: _darkShader5,
|
||||
shader6: _darkShader6,
|
||||
shader7: _white,
|
||||
bg1: const Color(0xFFD5A200),
|
||||
bg2: _black,
|
||||
bg3: const Color(0xff4f4f4f),
|
||||
bg4: const Color(0xff2c144b),
|
||||
tint1: const Color(0xff8738F5),
|
||||
tint2: const Color(0xffE6336E),
|
||||
tint3: const Color(0xffFF2D9E),
|
||||
tint4: const Color(0xffE9973E),
|
||||
tint5: const Color(0xffFBF000),
|
||||
tint6: const Color(0xffC0F000),
|
||||
tint7: const Color(0xff15F74E),
|
||||
tint8: const Color(0xff00F0E2),
|
||||
tint9: const Color(0xff00BCF0),
|
||||
main1: _darkMain1,
|
||||
main2: const Color(0xffe0196f),
|
||||
shadow: _black,
|
||||
sidebarBg: const Color(0xff232B38),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
icon: _darkShader5,
|
||||
text: _darkShader5,
|
||||
input: _darkInput,
|
||||
hint: _darkShader5,
|
||||
primary: _darkMain1,
|
||||
onPrimary: _darkShader1,
|
||||
hoverBG1: _darkMain1,
|
||||
hoverBG2: _darkMain1,
|
||||
hoverBG3: _darkShader3,
|
||||
hoverFG: _darkShader1,
|
||||
questionBubbleBG: _darkShader3,
|
||||
progressBarBGcolor: _darkShader3,
|
||||
toolbarColor: _darkInput);
|
||||
surface: const Color(0xff292929),
|
||||
hover: const Color(0xff1f1f1f),
|
||||
selector: const Color(0xff333333),
|
||||
red: const Color(0xfffb006d),
|
||||
yellow: const Color(0xffffd667),
|
||||
green: const Color(0xff66cf80),
|
||||
shader1: _white,
|
||||
shader2: _darkShader2,
|
||||
shader3: const Color(0xff828282),
|
||||
shader4: const Color(0xffbdbdbd),
|
||||
shader5: _darkShader5,
|
||||
shader6: _darkShader6,
|
||||
shader7: _white,
|
||||
bg1: const Color(0xFFD5A200),
|
||||
bg2: _black,
|
||||
bg3: const Color(0xff4f4f4f),
|
||||
bg4: const Color(0xff2c144b),
|
||||
tint1: const Color(0xff8738F5),
|
||||
tint2: const Color(0xffE6336E),
|
||||
tint3: const Color(0xffFF2D9E),
|
||||
tint4: const Color(0xffE9973E),
|
||||
tint5: const Color(0xffFBF000),
|
||||
tint6: const Color(0xffC0F000),
|
||||
tint7: const Color(0xff15F74E),
|
||||
tint8: const Color(0xff00F0E2),
|
||||
tint9: const Color(0xff00BCF0),
|
||||
main1: _darkMain1,
|
||||
main2: const Color(0xffe0196f),
|
||||
shadow: _black,
|
||||
sidebarBg: const Color(0xff232B38),
|
||||
divider: _darkShader3,
|
||||
topbarBg: _darkShader1,
|
||||
icon: _darkShader5,
|
||||
text: _darkShader5,
|
||||
input: _darkInput,
|
||||
hint: _darkShader5,
|
||||
primary: _darkMain1,
|
||||
onPrimary: _darkShader1,
|
||||
hoverBG1: _darkMain1,
|
||||
hoverBG2: _darkMain1,
|
||||
hoverBG3: _darkShader3,
|
||||
hoverFG: _darkShader1,
|
||||
questionBubbleBG: _darkShader3,
|
||||
progressBarBGColor: _darkShader3,
|
||||
toolbarColor: _darkInput,
|
||||
toggleButtonBGColor: _darkShader1,
|
||||
);
|
||||
}
|
||||
+6
-3
@@ -9,6 +9,7 @@ const _lightBg1 = Color(0xfff7f8fc);
|
||||
const _lightBg2 = Color(0xffedeef2);
|
||||
const _lightShader1 = Color(0xff333333);
|
||||
const _lightShader3 = Color(0xff828282);
|
||||
const _lightShader5 = Color(0xffe0e0e0);
|
||||
const _lightShader6 = Color(0xfff2f2f2);
|
||||
const _lightMain1 = Color(0xff00bcf0);
|
||||
const _lightTint9 = Color(0xffe1fbff);
|
||||
@@ -33,7 +34,7 @@ class DefaultColorScheme extends FlowyColorScheme {
|
||||
shader2: const Color(0xff4f4f4f),
|
||||
shader3: _lightShader3,
|
||||
shader4: const Color(0xffbdbdbd),
|
||||
shader5: const Color(0xffe0e0e0),
|
||||
shader5: _lightShader5,
|
||||
shader6: _lightShader6,
|
||||
shader7: _lightShader1,
|
||||
bg1: _lightBg1,
|
||||
@@ -66,8 +67,9 @@ class DefaultColorScheme extends FlowyColorScheme {
|
||||
hoverFG: _lightShader1,
|
||||
questionBubbleBG: _lightSelector,
|
||||
hoverBG3: _lightShader6,
|
||||
progressBarBGcolor: _lightTint9,
|
||||
progressBarBGColor: _lightTint9,
|
||||
toolbarColor: _lightShader1,
|
||||
toggleButtonBGColor: _lightShader5,
|
||||
);
|
||||
|
||||
const DefaultColorScheme.dark()
|
||||
@@ -115,7 +117,8 @@ class DefaultColorScheme extends FlowyColorScheme {
|
||||
hoverBG3: _darkShader3,
|
||||
hoverFG: _darkShader1,
|
||||
questionBubbleBG: _darkShader3,
|
||||
progressBarBGcolor: _darkShader3,
|
||||
progressBarBGColor: _darkShader3,
|
||||
toolbarColor: _darkInput,
|
||||
toggleButtonBGColor: _darkShader1,
|
||||
);
|
||||
}
|
||||
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user