Compare commits

...
70 Commits
Author SHA1 Message Date
Lucas.Xu 71d1189048 chore: bump version 0.1.6 (#2665) 2023-05-31 13:12:49 +08:00
Nathan.fooo 757009a97d chore: revert reorder rows in grid (#2533) (#2669)
This reverts commit f8f0599462.
2023-05-31 11:21:33 +08:00
Lucas.Xuandnathan e8665dc76c feat: support exporting document data to afdoc format (#2610)
* feat: support exporting document data to afdoc format

* feat: export database

* fix: resolve comment issues

* fix: add error tips when exporting files failed

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-05-30 17:55:38 +08:00
Daniyar Nurmukhamet f022f9aa06 feature: changed url field ux to be directly editable (#2523)
* feature: changed url field ux to be directly editable

* feature: added link svg icon
2023-05-30 11:04:10 +08:00
Alex Wallen 32650b7941 Revert "feat: support regenerating ai results #2012 (#2528)" (#2642)
This reverts commit 1f1a97b21b.
2023-05-28 11:56:38 -10:00
Alex Wallen 5a7339b092 [Fix] Integration test flakes. (#2635)
* feat: add tearDown and tearDownAll to reset workspaces

* fix: register tearDown and tearDownAll in empty_document_test

* feat: register tearDown and tearDownAll in board_test

* feat: register tearDown and tearDownAll in open_ai
2023-05-28 10:23:20 -10:00
Nathan.fooo 7ca028942c chore: implement export handler (#2625)
* chore: implement export handler

* chore: fix export
2023-05-27 21:21:34 +08:00
Alex Wallen 6fbd88fe76 Revert "Revert "feat: support regenerating ai results #2012 (#2528)" (#2622)" (#2636)
This reverts commit 2b5ecf5609.
2023-05-26 14:00:20 -10:00
Alex Wallen 2b5ecf5609 Revert "feat: support regenerating ai results #2012 (#2528)" (#2622)
This reverts commit 1f1a97b21b.
2023-05-26 13:59:21 -10:00
GitStartandgitstart 18672b197c fix: Update Indonesian language from Bahasa to Bahasa Indonesia (#2555)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-05-19 20:00:25 +08:00
GitStartandgitstart 63a267ca6d fix: Text in Show menu tooltip does not get translated (#2563)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-05-19 19:42:06 +08:00
1f1a97b21b feat: support regenerating ai results #2012 (#2528)
* feat: added emoji and network image support

* fix: code cleanup and improvements

* feat: rewrite AI response #2012

* fix: code refactor, rewrite in smart edit

* chore: delete the redundant file

---------

Co-authored-by: ahmeduzair890 <ahmeduzair12123@gmail.com>
Co-authored-by: rizwan3395 <rizwan.ios33gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-05-19 19:26:39 +08:00
Nils Jacobsen ca1ce8e19e chore: update inlang.config.js (#2542) 2023-05-19 19:26:04 +08:00
Mathias Mogensen f8f0599462 feat: reorder rows in grid (#2533)
* feat: reorder rows in grid

Allows reordering of rows in a Grid where no current filter and sorting is applied

Closes: #1123

* fix: resolve review comments
2023-05-16 21:20:05 +08:00
Daniyar NurmukhametandLucas.Xu a4845e668d feature: added name icon (#2529)
* feature: added name icon

* feature: added generating color in the defined HSL ranges

* refactor: removed trailing commas for readibility

* fix: fixed trimming name and separated color picker into util file

* fix: changed to using where instead of trim

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* feat: simplify the color generator

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-05-15 19:11:43 +08:00
Daniyar Nurmukhamet dd5bc7808c feature: added making text longer (#2532)
* feature: added making text longer

* fix: corrected I10n
2023-05-15 17:01:37 +08:00
Richard Shiue a993a2a181 fix: remove icon button not working in popover (#2518) 2023-05-15 10:59:34 +08:00
Richard Shiue 99ad9dcaff fix: don't show whitespace when no options are selected (#2493) 2023-05-15 10:56:38 +08:00
Annie d44de62b90 chore: Update README.md
Add a screenshot of the calendar view
2023-05-12 17:12:00 +08:00
Lucas.Xu 33b5837f04 chore: update changelog.md (#2510) 2023-05-11 19:26:26 +08:00
Lucas.Xu 54f757d9b2 bump version 0.1.5 (#2506)
* chore: bump version 0.1.5

* fix: could not trigger slash menu after inserting an emoji

* Revert "feat: add hover effect on an event card (#2487)"

This reverts commit f0a4b4b77d.

* feat: add hover effect on an event card

* fix: #2469 duplicated cover
2023-05-11 19:04:36 +08:00
Nathan.fooo cf27409c8d fix: number sort (#2507) 2023-05-11 18:35:36 +08:00
Lucas.Xu bd5250ae2f chore: update editor v0.1.12 and format the readme (#2489) 2023-05-10 15:01:15 +08:00
Yijing Huang f0a4b4b77d feat: add hover effect on an event card (#2487)
* chore: delete unncessary openCard method in RowCardContainer

* chore: delete unnessary code and add comment
2023-05-10 10:19:52 +08:00
Richard Shiue 151ee89855 fix: show delete icon for document icon properly (#2475) 2023-05-09 22:35:33 +08:00
Richard Shiue 2b7282195b feat: improve sidebar item dragged appearance (#2471) 2023-05-09 22:33:42 +08:00
Richard Shiue 4a1714b537 feat: select which properties to show in calendar (#2482) 2023-05-09 22:26:25 +08:00
Richard Shiue 6731037a8c fix: wrong day of week (#2468) 2023-05-08 09:51:20 +08:00
Annie 06f056aa4b chore: Update README.md
Update product screenshots
2023-05-07 15:47:45 +08:00
Lucas.Xu a2e9d63f63 chore: bump version 0.1.4 (#2455) 2023-05-04 20:05:23 +08:00
Richard Shiue 5d0a349236 fix: default include time (#2444)
* fix: default include time

* chore: clarify logic and add comments
2023-05-04 12:41:48 +08:00
Richard ShiueandNathan.fooo db8d030a85 feat: show unscheduled events in calendar toolbar (#2411)
* refactor: use same show row detail function

* fix: adjust popover offset

* feat: show unscheduled events in toolbar

* chore: apply suggestions from Xazin

* refactor: refactor list item into separate widget

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2023-05-04 11:05:17 +08:00
Richard Shiue 7aac4bc90b fix: adjust layout setting string (#2423) 2023-05-04 10:04:24 +08:00
Richard Shiue 8f41a1023c fix: tests after change in friendly date format (#2449) 2023-05-03 19:57:33 +08:00
Nathan.fooo 5e2b090f46 fix: number format (#2443)
* fix: number format

* chore: disable some tests that relate to timezone

* fix: tests
2023-05-03 18:00:58 +08:00
d65a56151b feat: Date format (#2414)
* build: fetch update from upstream

* fix: date format

* style: remove log package

* style: remove unused import

* chore: remove whitespace changes

* chore: remove whitespace changes

* chore: remove whitespace changes

---------

Co-authored-by: parfait kouassi <parfait@gmail.com>
Co-authored-by: Alex Wallen <wallenstephen@outlook.com>
2023-05-03 16:27:56 +08:00
Lucas.Xu c672a2a8ba fix: hover color for change icon button (#2447) 2023-05-03 16:18:05 +08:00
Nathan.fooo 75262c0683 Fix/calendar event update (#2442)
* fix: update event pos on calendar

* fix: replace text field id with date field id
2023-05-03 15:47:09 +08:00
MihirandLucas.Xu 39b1ff0910 feat: insert below and replace in smart-edit highlights text (#2107)
* feat: insert below and replace in smart-edit highlights text

* test: added integration tests to validate insert below and replace in smart-edit highlights text

* refactor: using get_it to inject OpenAiRepository to inject mock repo in test

* fix: delete node does not propagate non null selection

* refactor: suggested changes and fixed bugs causing warning in github-ci

* fix: integration tests causing error in github-ci

* refactor: reverting redundant changes due to recent changes in repo

* refactor: reverting redundant changes due to recent changes in repo

* refactor: refactoring to workspace based integration testing.

* refactor: reverting redundant changes due to recent changes in repo

* chore: fix analysis issues

* chore: fix analysis issues

* chore: remove the unnecessary conversion

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-05-03 15:43:11 +08:00
alceil f9095cfc64 feat: AI - improve writing (#2257)
* feat: AI - Improve writing

* fix: new line in result

* formatted code
2023-05-03 15:29:36 +08:00
Destiny Saturday 9e3ad1a6be feat: sync name on change (AppFlowy-IO#2033) (#2065) 2023-05-03 15:29:17 +08:00
GitStartandgitstart 473eb8bcbe feat: Only show the change cover button and delete button when it's hovering. (#2145)
* feat: Only show the change cover button and delete button when it's hovering.

* feat: Only show the change cover button and delete button when it's hovering

---------

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-05-03 15:22:16 +08:00
GitStart 22f09ddcb7 feat: Remove CI job based on Ubuntu 18.04 (#2375)
* feat: Remove CI job based on Ubuntu 18.04

* feat: Remove CI job based on Ubuntu 18.04

Removed extra comments
2023-05-03 14:58:46 +08:00
Akarsh Jainandnathan 2e18c020bc fix: font size for Numbers property value on calendar. (#2435)
* fix: font size for Numbers property value on calendar.

* fix: card style

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-05-03 14:43:50 +08:00
Richard Shiueandnathan c320f6ef8a fix: local time (#2436)
* feat: use user local timezone

* fix: suggestion for date logic

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-05-03 13:05:10 +08:00
Nathan.fooo 8cacfb1d07 feat: use user local timezone (#2407) 2023-05-03 11:50:02 +08:00
Richard Shiue c4ff1a6290 feat: double click day cell to create event (#2424) 2023-05-03 10:14:12 +08:00
Richard Shiue a48fca159f feat: show title in row detail page (#2433) 2023-05-03 10:13:33 +08:00
Richard Shiue 95bc325e85 fix: text overflow on time in event card (#2434) 2023-05-03 10:11:16 +08:00
Richard Shiueandnathan d5b70c842b fix: sort the events in each day cell by time (#2412)
* fix: sort the events in each day cell by time

* fix: reorder issue in calendar day

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-05-02 13:44:52 +08:00
Yijing Huang 545e57448a fix: UI issues in dark mode (#2344)
* fix: date cell UI issue #2301

* chore: update font size toggle button hover effect

* chore: update more button background

* chore: update NumberTypeOptionWidge hover style

* chore: improve the hover color in _SelectOptionColorCell

* chore: update User setting UI

* fix: delete unused file

* fix: remove the hover effect when delete button is disable
2023-05-02 10:18:51 +08:00
Daniil Golubev 3277f179ef feat: add link to changes to message (#2374) 2023-05-01 10:56:44 -10:00
Akarsh Jain c6cd10dfcf fix: hover state issue in icon picker for dark mode workspace name. (#2395) 2023-05-01 08:05:44 -10:00
Daniil Golubev 4be98fdba0 feat: update ru-RU localization (#2397) 2023-05-01 11:28:13 +08:00
Richard Shiue df4a5ca059 fix: calendar ui adjustments (#2398) 2023-05-01 11:26:57 +08:00
77d58a81fd chore: grid row page detail redesign (#2351)
* chore: grid row page detail update

* chore: update row_detail.dart

Co-authored-by: Alex Wallen <wallenstephen@outlook.com>

* chore: more adaptive and code cleanup

* feat: duplicate row

* feat: duplicate calendar event

* fix: ci

* feat: show other options

* fix: show include time

* fix: add key in RowCard to avoid incorrect data when open the row page

---------

Co-authored-by: Alex Wallen <wallenstephen@outlook.com>
Co-authored-by: nathan <nathan@appflowy.io>
2023-04-30 20:50:24 +08:00
Richard Shiue ac07c259f1 fix: #2028 auto enter edit mode for text field. (#2096)" (#2337)
This reverts commit 5afdb5de35.
2023-04-29 16:35:45 +08:00
Richard Shiue cd1202d3dc feat: open the event details page when a new one is create (#2388)
* style: code style improvement

* chore: open cal event details page upon creation
2023-04-29 16:32:29 +08:00
Richard Shiue 28f22e4d61 fix: calendar dark mode UI fixes (#2353)
* chore: calendar dark mode UI fixes

* chore: bump calendar_view package version

* chore: adjust padding
2023-04-29 16:25:43 +08:00
GitStart 2b1b1c0f28 fix: distorted/shrunken emojis in Callout option in theDocument page (#2361) 2023-04-27 11:17:40 -10:00
Mohamed DevOps c8f7401470 chore: add Arabic translation (#2348)
* add arabic translation

* add arabic locale

* add arabic translation

* Update ar-SA.json

* Update ar-SA.json
2023-04-27 13:41:26 +08:00
Yijing HuangandAkarsh Jain 37ba5d8e43 fix: fix icon hover color in dark mode (#2356)
* fix: File and Trash icons brightness in dark mode.
Fixes: #2298

* chore: update hover color in file location

* feat: add foregroundColorOnHover in HoverStyle

* feat: add iconColorOnHover in FlowyIconButton

* chore: update trash page UI

---------

Co-authored-by: Akarsh Jain <akarsh.jain.790@gmail.com>
2023-04-26 17:27:07 -10:00
GitStartandgitstart 857cd9ff55 fix: Use consistent path separators for workspace folder location in settings menu (#2349)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-04-25 20:02:04 +08:00
Yijing Huang fccd6135f7 feat: change the UI of language selector (#2317)
* feat: change the UI of language selector

* chore: clean up code

* chore: delete unnecessary code and clean up
2023-04-25 10:20:10 +08:00
Bram Tertoolen 49caf74ee1 ci: dont run flutter ci on commits that only change tauri sources (#2338) 2023-04-24 10:29:57 +08:00
Richard Shiue aaa3eb9041 fix: remove padding on popovers of newly-created fields (#2268) 2023-04-24 10:22:09 +08:00
Richard Shiue 9e47603360 fix: #2102 hide include time setting in date field options (#2266) 2023-04-24 10:21:32 +08:00
Annie 71bff8f864 feat: add a new entry to the question bubble (#2334) 2023-04-24 09:53:00 +08:00
Shubham Rawat 2af2621b49 feat: add url validation in cell_editor (#2287)
* feat: add url validation in cell_editor

* fix: url validation in cell editor

* feat: add UriFailure in url_validator
2023-04-23 17:26:27 +08:00
nathan a604c0f238 chore: bump version 2023-04-22 13:13:06 +08:00
138 changed files with 3506 additions and 1697 deletions

No files matched your search

+2
View File
@@ -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"
+1 -9
View File
@@ -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
+3 -1
View File
@@ -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}}
+27
View File
@@ -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
+5 -3
View File
@@ -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
+1 -1
View File
@@ -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

@@ -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;
}
@@ -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),
);
},
@@ -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();
}
@@ -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;
}
}
@@ -236,7 +236,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
},
);
_databaseController.addListener(
_databaseController.setListener(
onDatabaseChanged: onDatabaseChanged,
onGroupChanged: onGroupChanged,
);
@@ -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,
@@ -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,
});
}
@@ -51,5 +51,6 @@ class CalendarSettingEvent with _$CalendarSettingEvent {
}
enum CalendarSettingAction {
properties,
layout,
}
@@ -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;
}
}
@@ -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,
);
},
);
}
@@ -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();
@@ -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();
}
@@ -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 {
@@ -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;
@@ -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,
@@ -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(),
),
),
);
@@ -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'),
);
}
}
@@ -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,
@@ -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(
@@ -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(
@@ -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,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,
);
@@ -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;
}
}
@@ -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()),
),
),
);
},
@@ -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(
@@ -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,
),
),
);
@@ -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;
@@ -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) {
@@ -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,
),
@@ -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),
@@ -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),
);
}
@@ -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,
@@ -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,
@@ -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);
@@ -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(
@@ -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()
],
),
),
),
);
@@ -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'),
);
}
}
@@ -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;
}
@@ -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,
@@ -50,6 +50,7 @@ abstract class OpenAIRepository {
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
});
/// Get edits from GPT-3
@@ -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();
}
}
}
@@ -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,
@@ -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 = [];
@@ -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;
}
@@ -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,
@@ -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);
@@ -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(),
),
);
},
);
},
),
),
],
),
);
}
}
@@ -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();
@@ -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();
}
@@ -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,
);
}
}
@@ -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);
}
},
),
);
}
@@ -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),
);
}
}
@@ -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,
@@ -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,
);
}
@@ -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