Compare commits

..

573 commits
0.8.0 ... main

Author SHA1 Message Date
Richard Shiue
f9f151e89e
feat: implement afdivider (#7803) 2025-04-22 22:14:03 +08:00
Nathan
6bdaee3a00 chore: set toggle default value 2025-04-22 14:10:02 +08:00
Nathan.fooo
7f74543125
Merge pull request #7797 from AppFlowy-IO/integrate_workspace_template
Integrate workspace template
2025-04-22 13:35:03 +08:00
Nathan
514eeb8466 chore: init local ai when switching workspace 2025-04-22 12:16:59 +08:00
Nathan
403f343371 chore: delete supabase test 2025-04-22 12:16:59 +08:00
Lucas
8c5547da64
fix: custom font doesn't apply to settings page (#7801)
* fix: custom font doesn't apply to settings page

* chore: update settings page icons

* fix: add try catch in http services
2025-04-22 12:05:53 +08:00
Lucas.Xu
eaac387c8d fix: remove space migration 2025-04-22 11:26:08 +08:00
Lucas.Xu
00aad4da47 Merge branch 'integrate_workspace_template' of https://github.com/AppFlowy-IO/AppFlowy into integrate_workspace_template 2025-04-22 11:04:04 +08:00
Lucas.Xu
c10c844fa9 Merge branch 'main' into integrate_workspace_template 2025-04-22 10:59:39 +08:00
Nathan
40c1ae1d38 fix: click two times to enable local ai 2025-04-22 10:13:24 +08:00
Lucas
3ae6888fee
feat: invite member by link (#7780)
* feat: invite member by link

* feat: add invite by link section

* feat: integrate invite by link and copy invite link

* feat: integrate invite link apis

* feat: add reset link dialog

* feat: support redirect to admin panel

* fix: flutter analyze

* feat: remove expire time

* fix: apply correct color in dark mode

* fix: flutter analyze

* chore: disable theme hotkey test
2025-04-22 09:58:13 +08:00
Lucas
14b5e4e184
feat: add loading indicator in continue to sign in button (#7799) 2025-04-22 09:55:10 +08:00
Nathan
530e076838 chore: update test 2025-04-22 00:09:43 +08:00
Nathan
1be51d6679 chore: clippy 2025-04-21 22:26:53 +08:00
Nathan
b0c2b04a2d fix: empty view id 2025-04-21 22:22:03 +08:00
Nathan
4e2990e599 chore: remove local model 2025-04-21 22:02:06 +08:00
Nathan
9cd49c2447 chore: create workspace member 2025-04-21 21:07:38 +08:00
Morn
0cdecee771
fix: canLaunchUrl doesn't work with Flatpak (#7796) 2025-04-21 20:38:52 +08:00
Lucas.Xu
1bcf4e6e8d Revert "fix: left side bar does not reflect right workspace content"
This reverts commit 8cd0442a1f.
2025-04-21 17:20:44 +08:00
Lucas.Xu
343f7e4fd5 Revert "fix: loading exception when switching workspace"
This reverts commit 10a536b1a2.
2025-04-21 17:20:35 +08:00
Nathan
4634b51edf chore: open workspace with workspace auth type 2025-04-21 16:47:40 +08:00
Nathan
8cd0442a1f fix: left side bar does not reflect right workspace content 2025-04-21 16:47:27 +08:00
Nathan
10a536b1a2 fix: loading exception when switching workspace 2025-04-21 16:37:02 +08:00
Nathan.fooo
65b7916a6a
Merge pull request #7794 from AppFlowy-IO/optimize_write_user_workspaces
refactor: only notify when user workspaces were changed
2025-04-21 13:09:13 +08:00
Nathan
2cf96a2ce3 chore: fix rust test 2025-04-21 13:07:50 +08:00
Nathan
7885cb80f4 chore: fix rust test 2025-04-21 12:03:01 +08:00
Nathan
1356382524 refactor: only notify when user workspaces were changed 2025-04-21 11:41:58 +08:00
Morn
04407fe8ff
fix: issues with displaying mention text (#7773)
* fix: some mention text can not display correctly

* fix: remove the image widget from bookmark if the image url is null
2025-04-21 10:45:27 +08:00
David Woods
3451100b80
chore: bumped device_info_plus to 11.2.2 (#7782)
* bumped device_info_plus to 11.2.2 -- this version avoids the crash that happens when getInfo() is called on Windows in a VM or with Hyper-V active

* chore: bumped device_info_plus to 11.2.2 -- this version avoids the crash that happens when getInfo() is called on Windows in a VM or with Hyper-V active
2025-04-21 10:22:22 +08:00
Morn
f8927b1843
fix: crash when trying to delete emoji (#7787)
* fix: emoji picker error on desktop

* fix: test errors
2025-04-21 10:22:02 +08:00
Nathan.fooo
c7bf8bb1ba
Merge pull request #7789 from AppFlowy-IO/imple_local_unsupprot
chore: implement local unsupported methods
2025-04-20 20:52:27 +08:00
Nathan
c6010a6734 chore: fmt 2025-04-20 19:51:12 +08:00
Nathan.fooo
cf46213e00
chore: Update frontend/rust-lib/flowy-folder/src/manager.rs
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-04-20 19:34:05 +08:00
Nathan
2ee786f351 chore: update logs 2025-04-20 19:32:52 +08:00
Nathan
92d5690bba chore: pass folder init data 2025-04-20 18:07:02 +08:00
Nathan
791a79a234 chore: impl local unspport 2025-04-20 17:29:57 +08:00
Nathan
fa798f3ecd chore: workspace usage 2025-04-20 15:54:37 +08:00
Nathan.fooo
f72739d98d
Merge pull request #7788 from AppFlowy-IO/local_auth_type
refactor: local auth type
2025-04-20 15:17:14 +08:00
Nathan
fd581b4453 chore: clippy 2025-04-20 14:37:21 +08:00
Nathan
747a63d452 chore: create user workspace for anon user 2025-04-20 14:34:30 +08:00
Nathan
833a2bf5d6 Merge branch 'main' into local_auth_type 2025-04-20 13:36:18 +08:00
Nathan.fooo
607b7ecd1f
Merge pull request #7766 from AppFlowy-IO/private_database_inline_view
chore: remove inline view id reference
2025-04-20 13:35:53 +08:00
Nathan
bb5d36402a chore: fix test 2025-04-20 13:35:06 +08:00
Nathan
ccd1f5f8e9 chore: revert pod lock 2025-04-20 12:47:55 +08:00
Nathan
2c5f41b580 fix: set auth type 2025-04-20 12:47:14 +08:00
Nathan
2f5b494885 chore: remove authticator pb 2025-04-20 00:48:31 +08:00
Nathan.fooo
58f87b39aa
Merge pull request #7785 from AppFlowy-IO/auth_type_per_workspace
refactor: workspace setting and auth type
2025-04-20 00:24:45 +08:00
Nathan
d478ecfd41 chore: remove unused code 2025-04-19 23:33:34 +08:00
Nathan
72fc0cce07 chore: move sql 2025-04-19 22:18:15 +08:00
Nathan
81f63bebe6 chore: sql 2025-04-19 21:03:50 +08:00
Nathan
102087537a chore: fmt 2025-04-19 15:50:42 +08:00
Nathan
6dac45172e chore: auth type 2025-04-19 15:34:06 +08:00
Richard Shiue
84952b9056
chore: adjust generate theme script to import subtle colors (#7784) 2025-04-19 14:24:32 +08:00
Nathan
e851fba71b chore: clippy 2025-04-19 14:21:22 +08:00
Nathan
3a05a4851f chore: clippy 2025-04-19 14:07:49 +08:00
Nathan
edc5710e32 chore: auth type and remove unused code 2025-04-19 14:00:51 +08:00
Richard Shiue
1802792795
Merge pull request #7778 from AppFlowy-IO/feat/custom-prompt
feat: merge develop branch
2025-04-18 21:34:10 +08:00
Richard Shiue
28e89beb43 feat: appflowy theme lerp (#7771)
* feat: implement appflowy theme lerping

* refactor: use theme builder and adjust script

* chore: rename theme data

* chore: add doc comments

* chore: rename function

* chore: don't use theme extension

* chore: use the animated appflowy theme widget

* chore: clean up inherited theme
2025-04-18 21:06:46 +08:00
Richard Shiue
889756ebb0 refactor: use script to generate design tokens (#7751)
* refactor: use script to generate design tokens

* chore: improve code readaility

* refactor: make builder reusable to built in themes

* chore: improve code readability
2025-04-18 21:06:46 +08:00
Richard Shiue
54b5e248e3 feat: implement modal (#7750) 2025-04-18 21:06:46 +08:00
Nathan.fooo
d24383b6ea
Merge pull request #7779 from AppFlowy-IO/server_type
refactor: server type name
2025-04-18 18:55:10 +08:00
Nathan
2dc22004a1 refactor: remove server type 2025-04-18 15:57:33 +08:00
Nathan
0906febe95 refactor: remove server type 2025-04-18 15:48:17 +08:00
Richard Shiue
b12bd8ee85 feat: add medium sized text field (#7737)
* feat: add medium sized text field

* chore: remove height
2025-04-18 15:36:39 +08:00
Richard Shiue
e1bfb7095b feat: improve select modal button (#7736) 2025-04-18 15:35:21 +08:00
Richard Shiue
068f93c258 feat: add shadow tokens (#7726) 2025-04-18 15:35:21 +08:00
Richard Shiue
d8401e09c9 feat: implement keyboard triggering on buttons and add focus state (#7724)
* feat: implement keyboard triggering on buttons and add focus state

* chore: pass isFocused to builders
2025-04-18 15:35:21 +08:00
Nathan
394ac85c32 refactor: server type name 2025-04-18 15:31:50 +08:00
Nathan.fooo
f6e3290aa4
Merge pull request #7777 from AppFlowy-IO/prepare_auto_sync_chat
refactor: save chat and chat message
2025-04-18 15:31:23 +08:00
Nathan
4925e99166 chore: fix compile 2025-04-18 15:02:02 +08:00
Lucas
3bb5075a98
feat: setup/change password in settings page (#7752)
* feat: change password in settings page

* feat: add change password api

* feat: add password service

* feat: add setup password

* feat: refacotor account page

* chor: update i18n

* chore: i18n

* chore: i18n

* feat: add error message under text field

* fix: flutter tests

* chore: remove long password test

* fix: cloud integration test

* fix: cargo clippy

* fix: replace border color
2025-04-18 14:46:46 +08:00
Nathan
59efb7d9e5 chore: fix test 2025-04-18 14:43:01 +08:00
Nathan
165e95c480 chore: fix test 2025-04-18 14:36:47 +08:00
Nathan
31d8653ba6 refactor: save chat and chat message 2025-04-18 13:34:13 +08:00
Nathan.fooo
80df4955e2
Merge pull request #7754 from AppFlowy-IO/local_chat2
feat: support anon local ai chat/writer
2025-04-17 21:48:05 +08:00
Nathan
5436277ada chore: fmt 2025-04-17 21:39:49 +08:00
Nathan
ed64719560 chore: clippy 2025-04-17 21:09:24 +08:00
Nathan
ac659066c6 chore: return local model 2025-04-17 20:49:24 +08:00
Nathan
3a4d17f054 chore: enable anon ai writer 2025-04-17 16:54:02 +08:00
Nathan
57e4d269eb chore: enable local chat 2025-04-17 16:27:53 +08:00
Nathan
c2339c3522 Merge branch 'main' into local_chat2 2025-04-17 15:49:12 +08:00
Nathan
13065ac726 chore: add tests 2025-04-17 15:47:17 +08:00
Nathan
af91a72187 chore: select message 2025-04-17 14:07:01 +08:00
Nathan
98b835227e chore: remove unused code 2025-04-17 11:22:14 +08:00
Nathan
c633dd0919 chore: remove unused code 2025-04-17 11:11:54 +08:00
Morn
fbf928e6e4
chore: update CHANGELOG.md (#7765) 2025-04-17 11:01:30 +08:00
Lucas
8f63667282
feat: upgrade ubuntu version to 22.04 in github action (#7764)
* feat: upgrade ubuntu version to 22.04 in github action

* fix: cargo clippy
2025-04-17 09:48:23 +08:00
Nathan
e2896b2911 chore: remove inline view id reference 2025-04-16 22:03:14 +08:00
Nathan
77fbf0f8a3 chore: local ai initialize 2025-04-16 21:26:09 +08:00
Nathan
c89f33e2f8 chore: remove unused code 2025-04-16 20:59:25 +08:00
Nathan
e5b6393257 Merge branch 'main' into local_chat2 2025-04-16 20:23:55 +08:00
Nathan.fooo
954e844a21
Merge pull request #7761 from AppFlowy-IO/do_not_initialize_collab
fix: 0.8.9 release bugs
2025-04-16 17:06:31 +08:00
Morn
13acc3af86
fix: auto focus on link name textfield after open the link_edit_menu (#7757)
* fix: auto focus on link name textfield after open the link_edit_menu

* chore: update pubspec.yaml
2025-04-16 16:56:37 +08:00
Nathan
079112c9d2 chore: clippy 2025-04-16 16:30:07 +08:00
Nathan
0be8dcc000 chore: remove duplciate 2025-04-16 16:29:28 +08:00
Nathan
d7d040b0f9 Merge branch 'main' into do_not_initialize_collab 2025-04-16 16:08:29 +08:00
Nathan
f652229718 fix: open relation database row 2025-04-16 16:08:11 +08:00
Lucas
f727dde74b
fix: lose nested children when accepting AI responses (#7760)
* chore: add gitkeep in assets/font dir

* Revert "feat: improve white label scripts on Windows (#7755)"

This reverts commit a5eb2cdd9a.

* chore: use --verbose

* fix: lose nested children when accept ai response

* chore: lock analyzer version

* Reapply "feat: improve white label scripts on Windows (#7755)"

This reverts commit c73186306e.

* chore: lock analyzer version

* chore: update editor version
2025-04-16 15:59:43 +08:00
Nathan
92179fe61c chore: do not get workspace usage when current user is not owner 2025-04-16 15:16:06 +08:00
Nathan
ecfcf3be4d chore: commit pub lock 2025-04-16 14:37:45 +08:00
Nathan
be132d867a chore: lock analyzer version 2025-04-16 14:31:10 +08:00
Nathan
e6951012f0 chore: do not generate search summary if result is empty 2025-04-16 14:21:16 +08:00
Nathan
3214ec075b chore: fix flaky initialize folder indexer 2025-04-16 14:21:16 +08:00
Nathan
be1d2b4b92 chore: delay collab initialization until schema is configured; finalize to ensure collab includes its schema 2025-04-16 14:21:16 +08:00
Lucas
a5eb2cdd9a
feat: improve white label scripts on Windows (#7755)
* feat: improve white label scripts on Windows

* feat: add font white label script

* chore: integrate font white label script
2025-04-16 10:15:40 +08:00
Morn
9b077969e7
fix: error position for slash search result (#7758) 2025-04-16 10:11:31 +08:00
Nathan.fooo
7160790596
Merge pull request #7748 from AppFlowy-IO/adjust_search_ui
chore: loading for search summary
2025-04-15 22:52:49 +08:00
Nathan
5c3e81e6dc chore: adjust ui 2025-04-15 22:49:34 +08:00
Nathan
846172a709 chore: adjust ui 2025-04-15 22:19:16 +08:00
Nathan
f62686fdeb chore: support local chat 2025-04-15 21:07:01 +08:00
Morn
c6511cfb55
fix: mobile slash menu position error (#7747)
* fix: mobile slash menu position error

* fix: mobile slash menu sometimes not showing up
2025-04-15 17:43:50 +08:00
Richard Shiue
69b5452af5
fix: undo not working in database row full page (#7749) 2025-04-15 17:17:39 +08:00
Nathan
9291236733 chore: clippy 2025-04-15 16:49:33 +08:00
Nathan
3b3ae7fde9 chore: loading for search summary 2025-04-15 15:59:15 +08:00
Morn
d9748d5ef1
fix: block does not expand with grid size (#7745)
* fix: block does not expand with grid size

* fix: replace listenForSizeChanged with SizeChangedLayoutNotifier
2025-04-15 13:50:53 +08:00
Richard Shiue
d01909830d
fix: ai writer not working in database rows (#7746)
* fix: ai writer not working in database rows

* chore: dart analysis
2025-04-15 11:14:20 +08:00
Nathan.fooo
03ecc3d6c9
Merge pull request #7727 from AppFlowy-IO/search_summary
chore: search summary
2025-04-15 10:37:17 +08:00
Lucas
1630e11c4d
chore: update login page icons and i18n (#7742)
* feat: update i18n and icons

* chore: replace appflowy with welcome to appflowy

* fix: protenial delete page error

* fix: flutter analyze
2025-04-15 09:39:33 +08:00
Nathan
99fb6ab743 chore: fix linux watch 2025-04-14 23:07:42 +08:00
Nathan
35bc095760 chore: local and server result 2025-04-14 22:58:37 +08:00
Nathan
a44ad63230 chore: display preview 2025-04-14 16:40:54 +08:00
Lucas
ddbaf0d530
feat: otp improvement on mobile (#7738) 2025-04-14 14:58:00 +08:00
Nathan
437deaf986 chore: update logs 2025-04-14 14:38:25 +08:00
Morn
54df6b2863
fix: link_preview launch review issues (#7731)
* fix: some link_preview launch review issues

* fix: some UI issues

* chore: pasting a link will not check whether it is an image

* fix: copy link to block not supported well

* fix: mention UI issues

* feat: support get youtube channel info

* chore: update translation

* feat: add shadow in appflowy theme

* chore: remove AFThemeExtensionV2

* fix: some UI issues
2025-04-14 14:14:05 +08:00
Richard Shiue
69b98cb323
fix: open board row as page (#7735) 2025-04-14 10:47:55 +08:00
Nathan
4d172761ce chore: search with ai summary 2025-04-14 10:36:42 +08:00
Lucas
8c4324ee9d
fix: otp launch review issues (#7730)
* fix: add error text under text field

* chore: update translation
2025-04-14 10:29:46 +08:00
Lucas
2e295e6891
fix: unable to accept the response from 'mark longer' (#7725)
* fix: unable to accpet 'make it longer'

* fix: markdown text robot test
2025-04-11 14:25:19 +08:00
Richard Shiue
351c891a5a
fix: adjust ghost icon hover color (#7723) 2025-04-11 10:21:02 +08:00
Richard Shiue
bcb1e8e4f5
chore: adjust replace, insert below, discard selection behavior (#7719) 2025-04-11 09:57:41 +08:00
Morn
4997ac99cf
feat: revamp link preivew (#7692)
* feat: revamp link preivew

* feat: add convert to menu for link hover menu

* feat: add mention link

* feat: support convert preview to mention

* feat: add embed link preview

* fix: some test erros

* fix: test errors

* fix: some UI issues

* chore: add test for url

* chore: add test for mention

* chore: add test for bookmark

* chore: add test for embed

* chore: remove unuse import

* fix: some UI issues

* fix: remove text span overlay on mobile

* fix: code lint error

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2025-04-10 14:45:13 +08:00
Lucas
e3bbabd63e
feat: sign in with passcode (#7685)
* feat: sign in with password or passcode

* feat: add loading dialog

* chore: update translations

* feat: support otp on mobile
2025-04-10 14:06:08 +08:00
Morn
403c558f9b
chore: update translation (#7675) 2025-04-10 13:45:22 +08:00
Nathan.fooo
31d2c1d603
Merge pull request #7718 from AppFlowy-IO/remove_path_to_lai
chore: remove local ai related path to lai repo
2025-04-09 21:56:40 +08:00
Nathan
a7adeeadb1 chore: remove local ai related path to lai repo 2025-04-09 20:47:03 +08:00
Nathan.fooo
f7d7141a59
Merge pull request #7717 from AppFlowy-IO/fix_local_ai_setting
chore: fix local ai setting
2025-04-09 20:43:36 +08:00
Nathan
2371d4691d chore: fix local ai setting 2025-04-09 20:07:06 +08:00
Nathan
9ec0437673 chore: remove enable sync log toggle 2025-04-09 14:15:48 +08:00
Richard Shiue
89525b7f7b
chore: bump version (#7716) 2025-04-09 14:11:33 +08:00
Richard Shiue
ad227bcf79
chore: toast message when theme folder doesn't have permission (#7715) 2025-04-09 13:45:07 +08:00
Lucas
8b82d532e0
fix: replace the selected text with ai response in the same line (#7708)
* fix: replace the selected text with ai response in the same line

* fix: replace the selected text with ai response in the multiple lines

* fix: integrate the replace function in the ai writer cubit

* fix: unit test and integration test
2025-04-09 13:21:19 +08:00
Morn
f4af47728f
fix: overwriting a selected text removes more than expected (#7714) 2025-04-09 12:57:58 +08:00
Richard Shiue
b3a4a9ba04
fix: use normal underline for AI suggested text style (#7712) 2025-04-09 10:25:29 +08:00
Lucas
13028c6cfe
fix: icon and emoji doesn't align in mention page menu (#7673) 2025-04-09 09:39:35 +08:00
Richard Shiue
7d0f6c9deb
fix(mobile): edit mention page (#7698)
* fix(mobile): edit mention page

* chore: type check

* chore: replace usages

* chore: use MentionType instead
2025-04-09 09:38:42 +08:00
FakhriAzzouz
8d2901cc58
chore: arabic translations update (#7701)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-04-09 09:38:00 +08:00
Aniket Patil
bbc7b6d172
feat(i18n): add initial Marathi (mr-IN) translations (#7667)
* feat(i18n): Add initial Marathi (mr-IN) translations

* fix(i18n): Force add mr-IN.json in assets folder for localization

* fix(i18n): Sync mr-IN.json from assets to resources

* fix(i18n): Final sync of full Marathi translation in assets

* feat(i18n): Force add Marathi translations to ignored directory

* feat(i18n): Complete Marathi translations
2025-04-09 09:37:21 +08:00
Lucas
145d1e5fdb
feat: support white label on windows (#7706) 2025-04-09 09:36:53 +08:00
Nathan.fooo
eae4e42dcc
Merge pull request #7709 from AppFlowy-IO/refactor_path_name
chore: rename data path
2025-04-08 21:19:07 +08:00
Nathan
6886261692 chore: ensure path 2025-04-08 20:49:15 +08:00
Nathan
23f2d85e70 chore: clippy 2025-04-08 20:23:25 +08:00
Nathan
d1d598940d chore: clippy 2025-04-08 16:08:28 +08:00
Nathan
efb98d28ef chore: rename data path 2025-04-08 15:53:34 +08:00
Richard Shiue
462c822255
fix: translations (#7694) 2025-04-08 13:08:10 +08:00
Richard Shiue
9e30b1816f
fix(flutter_desktop): grid bug (#7697)
* fix: field width doesn't persist

* test: add test

* test: fix test

* test: grid width integration test
2025-04-08 11:46:06 +08:00
Nathan.fooo
d79929d1c9
Merge pull request #7700 from AppFlowy-IO/sync_log_enable_default
chore: enable sync log by default
2025-04-08 10:31:01 +08:00
Nathan
0286678286 chore: clippy 2025-04-08 10:30:48 +08:00
Nathan
4896d7c1be chore: enable sync log by default 2025-04-07 22:04:32 +08:00
Nathan.fooo
b06a2337a0
Merge pull request #7699 from AppFlowy-IO/use_uuid
Chore: Use UUID instead of using String
2025-04-07 21:54:48 +08:00
Nathan
20bcdd1f90 chore: fmt 2025-04-07 21:28:48 +08:00
Nathan
24d57336a9 chore: bump collab id 2025-04-07 21:15:30 +08:00
Nathan
91397c963a chore: clippy 2025-04-07 19:34:26 +08:00
Nathan
995b773c74 chore: replace str with uuid 2025-04-07 19:24:58 +08:00
Richard Shiue
c561abd9f8
fix: clear text field upon selection (#7695) 2025-04-07 14:52:44 +08:00
Lucas
10dd0fa438
feat: implement new color tokens design (#7684)
* fix: missing AFThemeExtensionV2 on mobile

* feat: add appflowy_ui package
2025-04-04 14:41:13 +08:00
Lucas
d74b0bf6e1
fix: only log document event when enableDocumentInternalLog is enabled (#7676) 2025-04-02 16:09:52 +08:00
Morn
913924d8d3
fix: emoji menu should only be triggered when “:” has a followed letter (#7672) 2025-04-02 14:15:14 +08:00
Nathan.fooo
7bc9ce4391
Merge pull request #7674 from AppFlowy-IO/custom_compare_func
fix: after applying different ai model, the selection menu doesn't show the new model
2025-04-02 13:59:04 +08:00
Nathan
ed5334a7d6 chore: clippy 2025-04-02 13:38:29 +08:00
Nathan
120f22c6fc chore: notify model change after applying local ai model 2025-04-02 13:08:45 +08:00
FakhriAzzouz
f8a17dac00
chore: update translations with Fink 🐦 (#7669) 2025-04-02 09:50:50 +08:00
Lucas
7f41feb959
chore: update changelog (#7671) 2025-04-01 21:41:20 +08:00
Nathan.fooo
da7f584da7
Merge pull request #7670 from richardshiue/fix/hotfix
fix: translation hotfix
2025-04-01 21:34:06 +08:00
Richard Shiue
031a88f4c4 fix: translation hotfix 2025-04-01 21:26:24 +08:00
Lucas
e2ff12415c
chore: update translation (#7666) 2025-04-01 17:35:22 +08:00
Morn
edd59cf462
fix: link menu issues (#7661)
* fix: duplicated link menu issue

* fix: support toolbar animation

* chore: update appflowy_editor

* fix: update pubspect.lock

* fix: testing error

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-04-01 14:17:04 +08:00
Morn
b52f681e3c
fix: launch review issues for non-search-bar emoji picker (#7654)
* fix: some launch review issues for emoji picker

* fix: revamp emoji picker which is created by character

* fix: add padding for non-searchbar emoji picker
2025-04-01 09:33:54 +08:00
Lucas
5d33d723e9
fix: the checklist item will disappear when reordering (#7659) 2025-04-01 09:25:49 +08:00
Morn
5e1a8b1ec7
fix: toolbar link launch review issues (#7639)
* fix: some toolbar link launch review issues

* fix: support check link format for link menus

* fix: toolbar and link hover menu will not display together

* fix: filter link search result with current document id

* fix: remove error text while link menu is not focus

* fix: some issues

* fix: test errors

* fix: add tooltip for link menu
2025-03-31 15:16:56 +08:00
Richard Shiue
3c99105b23
fix: show cursor at end of selection when generating (#7655) 2025-03-31 15:06:50 +08:00
Richard Shiue
0b298ea426
chore: use emoji instead of Local for copy (#7656) 2025-03-31 15:06:39 +08:00
Lucas
7ed36f8736
feat: support i18n in delete my account operation (#7635) 2025-03-31 13:07:42 +08:00
Richard Shiue
34d2b7f24e
chore: improve local ai settings page (#7651)
* chore: improve local ai settings page

* chore: move local to top in select list

* chore: improve copy of missing model

* chore: remove green background and add progress indicator

* chore: change text color
2025-03-31 12:05:11 +08:00
Richard Shiue
a2303d35e8
chore: hide formats when using specific ai writer commands (#7648)
* chore: hide formats for specific ai writer features

* chore: use black color for selected model name
2025-03-31 10:24:46 +08:00
Nathan.fooo
c87d9ab74f
Merge pull request #7652 from AppFlowy-IO/show_not_ready
chore: display message when using ai writer with local ai, but local ai  is disabled
2025-03-30 22:16:05 +08:00
Nathan
e3a0806eee chore: clippy 2025-03-30 20:53:27 +08:00
Nathan
b63c4dfe21 chore: show local ai disable 2025-03-30 20:12:20 +08:00
Nathan
2277d7d234 chore: show local ai disable 2025-03-30 15:15:59 +08:00
Nathan
f76ce2be14 chore: show not ready state when using ai writer with local ai 2025-03-30 12:06:08 +08:00
Nathan.fooo
3c74208ab9
Merge pull request #7650 from AppFlowy-IO/display_plugin_version
chore: show plugin version
2025-03-30 12:05:25 +08:00
Nathan
5ae3f42313 chore: fix windows build 2025-03-30 11:28:29 +08:00
Nathan
af5c4bfe76 chore: show plugin version 2025-03-30 10:48:26 +08:00
Nathan.fooo
9c25769d8e
Merge pull request #7649 from AppFlowy-IO/revert-7643-chore/improve-local-ai-setting-page
Revert "chore: improve local ai settings page"
2025-03-30 09:54:26 +08:00
Nathan.fooo
12a4bf67f6
Revert "feat: improve local ai settings page UI (#7643)"
This reverts commit 3a879b0186.
2025-03-30 09:54:08 +08:00
Nathan
34a858e948 chore: show plugin version 2025-03-30 09:02:06 +08:00
Richard Shiue
3a879b0186
feat: improve local ai settings page UI (#7643)
* chore: improve local ai settings page

* chore: improve local ai settings page

* chore: simplify state and improve ui
2025-03-29 21:56:05 +08:00
Nathan.fooo
dbe72b32b2
Merge pull request #7646 from AppFlowy-IO/show_lai_resource
chore: bump lai
2025-03-29 19:58:08 +08:00
Nathan
917aa60c98 chore: bump lai 2025-03-29 15:32:45 +08:00
Nathan.fooo
84e0f5e6ff
Merge pull request #7595 from AppFlowy-IO/mcp_protocol
chore: implement MCP client
2025-03-29 09:10:05 +08:00
Nathan
7b89d76cea chore: clippy 2025-03-28 23:31:00 +08:00
Nathan
fbf031b06d chore: bump local ai 2025-03-28 23:00:27 +08:00
Nathan
671e855b0e Merge branch 'main' into mcp_protocol 2025-03-28 16:27:19 +08:00
Nathan
8aa32ca3fa chore: disable mcp 2025-03-28 16:27:16 +08:00
Richard Shiue
07c767c4fa
fix: ai writer ux issues (#7640)
* chore: fix local ai font weight

* chore: adjust auto response format copy

* fix: escape shortcut

* chore: more action button icon size
2025-03-28 11:23:29 +08:00
Nathan.fooo
4ce0782a05
Merge pull request #7638 from AppFlowy-IO/ai_model_desc
Ai model desc
2025-03-27 22:07:00 +08:00
Nathan
7456c65799 chore: clippy 2025-03-27 20:54:48 +08:00
Nathan
8cf31b8afc chore: bump client api 2025-03-27 20:51:29 +08:00
Nathan
ff70595a15 chore: display ai model desc and fix flashing when select model 2025-03-27 20:03:07 +08:00
Nathan
f6f19a0a07 chore: display ai model desc on setting page 2025-03-27 19:17:52 +08:00
Richard Shiue
b83b964678 chore: add model description UI 2025-03-27 18:01:45 +08:00
Nathan
f574b6b9c2 chore: update default name 2025-03-27 17:10:35 +08:00
Nathan
7ee29dcbc5 chore: return model desc 2025-03-27 17:09:01 +08:00
Nathan
d348361889 chore: return desc of ai model 2025-03-27 16:31:18 +08:00
Richard Shiue
07a78b4ad7
chore: adjust default model name (#7634) 2025-03-27 14:26:45 +08:00
Morn
a26ebbccc1
fix: toolbar launch review issues (#7631)
* fix: keep the turn into menu within six-dot same as toolbar

* fix: change some icon color within toolbar

* fix: improve toolbar

* chore: update editor dependency

* fix: update editor dependency
2025-03-27 14:19:51 +08:00
Nathan
ccb020e885 chore: bump lai 2025-03-27 14:19:27 +08:00
Lucas
76cb23e233
feat: add bloc observer (#7633)
* chore: bump version 0.8.8

* feat: add bloc observer

* chore: update comment

* chore: update comment
2025-03-27 14:17:47 +08:00
Morn
4686e13390
feat: add animation for floating toolbar (#7623) 2025-03-27 13:28:35 +08:00
Nathan.fooo
584f762e11
Merge pull request #7617 from richardshiue/chore/improve-model-selection-ui
feat: regenerate message with different model
2025-03-27 12:54:26 +08:00
Richard Shiue
8528811992 chore: remove ai model class 2025-03-27 12:02:58 +08:00
Richard Shiue
9147f64b65
chore: adjust suggestion action button position (#7632) 2025-03-27 11:10:23 +08:00
Richard Shiue
f7f2e71ee1
chore: adjust popover shadows (#7630) 2025-03-27 10:05:26 +08:00
Richard Shiue
f9e1dcca6c chore: code nit 2025-03-27 00:15:48 +08:00
Richard Shiue
e11388491f chore: emit ready state faster 2025-03-27 00:15:48 +08:00
Richard Shiue
eb0cff36c9 chore: improve ai model selection ui 2025-03-27 00:15:48 +08:00
Nathan
ac8141ab15 Merge branch 'main' into mcp_protocol 2025-03-26 16:18:46 +08:00
Richard Shiue
b3b13e550d
chore: adjust popover shadows (#7626) 2025-03-26 14:29:11 +08:00
Nathan.fooo
ba1767e312
Merge pull request #7628 from AppFlowy-IO/local_ai_global_config
chore: implement local ai config
2025-03-26 14:29:02 +08:00
Nathan
10048dadec Merge branch 'main' into local_ai_global_config 2025-03-26 14:21:12 +08:00
Nathan
815bb11cde chore: implement local ai config 2025-03-26 14:19:57 +08:00
Lucas
7372f5583c
chore: bump version 0.8.8 (#7627) 2025-03-26 13:56:24 +08:00
Lucas
9115e208ac
fix: numbered list generated by ai should keep the same index as the input (#7622)
Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2025-03-26 13:31:32 +08:00
Morn
24bb1b58a0
feat: support use ":" keyword to create emojis (#7582)
* feat: add ability to use : keyword to create emojis(#2797)

* fix: emoji position error

* chore: add integration test

* chore: dismiss emoji picker while starting searching with space
2025-03-26 13:24:16 +08:00
Ametero
cfca70ae14
chore: update translations with Fink 🐦 (#7584) 2025-03-26 13:23:25 +08:00
Morn
1db6da7024
fix: undo not working for toggle heading (#7598) 2025-03-26 13:22:47 +08:00
Richard Shiue
c212c568e9
chore: bump editor ref (#7621) 2025-03-26 11:00:41 +08:00
Richard Shiue
1d437fb81e
chore: adjust ai writer popover content area (#7618) 2025-03-25 23:44:00 +08:00
Richard Shiue
5a0478ad56
fix(mobile): settings page crashes (#7616) 2025-03-25 23:00:28 +08:00
Richard Shiue
039c191d1f
fix: ai text insertion (#7615)
* fix: dont scroll to ai writer node if path not found

* chore: rename text robot clear method and add reset

* fix: insert position is off if using ai writer multiple times

* chore: reorganize code

* fix: undo not working after accept
2025-03-25 22:41:35 +08:00
Richard Shiue
eb4b015de8
chore: bump editor plugin ref (#7610) 2025-03-25 20:37:15 +08:00
Khor Shu Heng
5269bfbf8e
Merge pull request #7609 from ixicut/fix-ukrainian-translation-7603
fix: correct translation inconsistencies
2025-03-25 18:05:30 +08:00
ixicut
ed44c20281 fix: correct translation inconsistencies 2025-03-25 11:19:11 +02:00
Nathan.fooo
878323a299
Merge pull request #7606 from AppFlowy-IO/fix_exceptions
chore: fix incorrect local ai state
2025-03-25 13:59:49 +08:00
Nathan
0dc2363962 chore: fix incorrect local ai state 2025-03-25 13:05:18 +08:00
Richard Shiue
72d660f1ac
fix: number cell update flashes with old content (#7605) 2025-03-25 11:07:46 +08:00
Nathan.fooo
46532a861f
Merge pull request #7602 from AppFlowy-IO/remove_default_model
chore: remove default model name
2025-03-24 22:37:23 +08:00
Nathan
4c39908748 chore: separate model 2025-03-24 22:21:44 +08:00
Nathan
35081fd311 chore: remove default model name 2025-03-24 21:59:42 +08:00
Richard Shiue
7463e4e3eb
fix: pass response format in ai writer (#7599) 2025-03-24 16:50:16 +08:00
Nathan
66ce786726 chore: add client 2025-03-24 16:38:37 +08:00
Richard Shiue
682a50da53
chore: replace ai response every time (#7597) 2025-03-24 14:15:20 +08:00
Nathan
d372abd5a1 Merge branch 'main' into mcp_protocol 2025-03-24 13:56:56 +08:00
Richard Shiue
dfb5a6629f
chore: pass ai writer context (#7596)
* chore: pass ai writer context

* chore: maintain selection after starting ai writer

* chore: improve ui of additional comments

* chore: revert podfile.lock changes

* chore: code readability

* chore: revert podfile.lock changes

* fix: accept shouldn't try to unformat
2025-03-24 13:27:31 +08:00
Nathan.fooo
bb72f7e70a
Merge pull request #7590 from AppFlowy-IO/support_switch_model
chore: support switch ai model in chat/ AI writer
2025-03-24 12:40:43 +08:00
Nathan
37085042f8 chore: clippy 2025-03-24 12:40:29 +08:00
Nathan
2cbcb320fe chore: add timeout 2025-03-24 12:11:12 +08:00
Nathan
949556e2fa chore: remove tauri feature 2025-03-24 12:03:42 +08:00
Nathan
08d1d3602e chore: implement MCP client 2025-03-24 10:44:20 +08:00
Morn
910c45e457
chore: change some mobile slash menu icons (#7579) 2025-03-24 09:55:58 +08:00
Morn
6f031d0c7e
feat: revamp toolbar link (#7578)
* feat: revamp toolbar link

* fix: some review issues

* chore: add integration test for toolbar link
2025-03-24 09:55:23 +08:00
Morn
44c9d572c8
fix: using date picker with @ menu with some errors (#7580) 2025-03-24 09:54:45 +08:00
Nathan
05949d2f87 chore: support switch ai model in chat or ai writer 2025-03-23 21:53:05 +08:00
Nathan.fooo
ad695e43b9
Merge pull request #7575 from AppFlowy-IO/completion_stream_v2
chore: completion stream v2
2025-03-21 11:47:28 +08:00
Nathan
87015f7133 chore: upgrade lai commit 2025-03-21 11:47:05 +08:00
Richard Shiue
deb019aa4a chore: don't allow double register two ai nodes 2025-03-21 11:14:49 +08:00
Richard Shiue
c79d014305 chore: revert podfile changes and update error 2025-03-21 10:49:41 +08:00
Richard Shiue
182101023b chore: remove actions in explanation when not explain 2025-03-21 10:37:10 +08:00
Richard Shiue
f1b2f51a06 chore: fix test 2025-03-21 10:20:47 +08:00
Nathan
a5c0ad5998 chore: fix test 2025-03-21 10:07:27 +08:00
Nathan
6fd250d4d1 chore: update client api 2025-03-21 10:04:51 +08:00
Nathan
db2270c8d8 chore: update local ai commit 2025-03-20 15:13:25 +08:00
Richard Shiue
10f19069c6 chore: implement ui 2025-03-20 13:30:42 +08:00
Richard Shiue
13a7ea07a8 chore: merge remote-tracking branch 'upstream/main' into this one 2025-03-20 13:29:03 +08:00
Richard Shiue
aacd795ae0
chore: ai writer more actions (#7576)
* feat: add more button to ai writer input box

* chore: adjust button padding

* chore: adjust icon color
2025-03-20 12:33:51 +08:00
Richard Shiue
566e7b2f40
feat: allow user scroll during generation (#7559)
* chore: add keep alive to ai writer block component

* chore: allow user scrolling during ai writer generation

* chore: pull ai writer cubit upwards

* test: fix unit tests

* chore: clear selection
2025-03-20 11:50:25 +08:00
Nathan
f413b9e070 chore: callback 2025-03-20 11:44:02 +08:00
Nathan
6e4206a8e2 chore: completion stream v2 2025-03-20 11:41:49 +08:00
Nathan
954aa48f52 chore: bump lai commit 2025-03-19 14:17:17 +08:00
Richard Shiue
461ac91b32
fix: continue writing empty text case (#7574) 2025-03-19 14:13:20 +08:00
Nathan.fooo
8a9cc278ec
Merge pull request #7573 from AppFlowy-IO/rename_plugin
chore: rename the local ai plugin
2025-03-19 11:29:23 +08:00
Morn
9db87944f2
fix: toolbar tooltip message is incorrect on Windows (#7572)
* fix: some toolbar text display error

* chore: change string id to enum string id
2025-03-19 11:23:07 +08:00
Nathan
9230981e54 chore: remove test 2025-03-19 10:59:11 +08:00
Nathan
72b13dd941 chore: adjust UI 2025-03-19 10:48:30 +08:00
FakhriAzzouz
e6b0c8ff05
chore: update Arabic translations (#7571)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-03-19 10:45:57 +08:00
Nathan
f0d967f0e4 chore: rename the local ai plugin 2025-03-19 10:23:38 +08:00
Morn
85b9aab015
chore: update CHANGELOG.md (#7569) 2025-03-18 18:46:07 +08:00
Morn
69571f668c
fix: some launch review issues (#7566)
* chore: adjust some toolbar text

* chore: change the icons in turn-into menu

* chore: change the icon in color menu

* fix: keep selection after doing some changes from toolbar

* fix: color menu displaying error

* fix: wrong filter logic in toolbar suggestion menu

* fix: some launch review issues

* fix: test errors
2025-03-18 18:45:31 +08:00
Lucas
a89dd87c16
fix: optimize cover title position offset calculation (#7568)
* fix: optimize cover title position offset calculation

* feat: ingore shift+enter in callout/quote and fallback to system behavior
2025-03-18 17:53:21 +08:00
Richard Shiue
22b03eee29
chore: implement ai writer history (#7523)
* chore: implement ai writer history

* chore: pass hitosyr
2025-03-18 17:14:20 +08:00
Nathan.fooo
e3ea3fcdfa
Merge pull request #7570 from AppFlowy-IO/update_local_ai_translation
chore: update local ai translation
2025-03-18 16:43:39 +08:00
Nathan
ccfbde9a92 chore: update local ai translation 2025-03-18 16:42:29 +08:00
Nathan.fooo
7358860bfc
Merge pull request #7567 from AppFlowy-IO/fix_release_crash_2
chore: update commit
2025-03-18 16:07:25 +08:00
Nathan
17b355197c chore: update commit 2025-03-18 15:54:16 +08:00
Nathan.fooo
aa7e50cc6c
Merge pull request #7565 from AppFlowy-IO/update_translation
chore: update translation
2025-03-18 13:00:33 +08:00
Nathan
69ce105806 chore: update translation 2025-03-18 12:54:45 +08:00
Lucas
cafdfcca51
feat: add download button in file block (#7562) 2025-03-18 11:31:18 +08:00
NavyStack
a8c5c9c34e
chore(i18n): add and fine-tune ko-KR translations (#7548)
* feat(translations): reset hard Korean translation

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

* i18n(ko-KR): Add new translations and fine-tune existing ones

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

---------

Co-authored-by: fvoci <karl@hwan.dev>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>
2025-03-18 10:30:51 +08:00
Morn
6dd83675fc
fix: toolbar launch review issues (#7532)
* fix: some launch review issues

* fix: some launch review issues

* fix: color picker position error

* fix: redesign the dropdown arrow and padding

* feat: implement toolbar button state

* fix: keep custom color not changed

* feat: revamp color icon in toolbar

* fix: correct toolbar position & add animation for toolbar

* fix: ajust toolbar animation parameters

* chore: adjust some UI values

* fix: keep selection after turn into

* fix: hover color on toolbar is wrong in dark mode

* fix: toolbar icon color in dark mode
2025-03-17 18:03:23 +08:00
Lucas
b65fad6214
chore: update template link url (#7557) 2025-03-17 16:57:02 +08:00
Lucas
884046ba3f
fix: first added cover might be invisible (#7555) 2025-03-17 15:41:54 +08:00
Lucas
6d327adb83
fix: quote block flashes in ai chat page when generating answer (#7553) 2025-03-17 13:03:47 +08:00
Richard Shiue
eddb623fba
fix: message hover action flashing (#7552) 2025-03-17 11:02:42 +08:00
Lucas
2ea8e831cd
fix: the workflow of switching to Local in app (#7540)
* fix: unable to redo undo when the selection is null

* fix: the workflow of switching to Local in app

* fix: text color doesn't work in table cell

* fix: test
2025-03-17 09:36:46 +08:00
Nathan.fooo
270c981051
Merge pull request #7549 from AppFlowy-IO/fix_internal_stream
chore: bump client api
2025-03-16 20:07:48 +08:00
Nathan
7971566159 chore: bump client api 2025-03-16 19:45:51 +08:00
Nathan.fooo
9fbf2d5ef6
Merge pull request #7547 from AppFlowy-IO/related_question_height
chore: set releated question height
2025-03-16 13:34:34 +08:00
Nathan
21bf2968a9 chore: set releated question height 2025-03-16 12:14:11 +08:00
Nathan.fooo
117950c922
Merge pull request #7546 from AppFlowy-IO/local_embed
chore: local ai embed file
2025-03-16 12:12:45 +08:00
Nathan
b4c56f7998 Merge branch 'main' into local_embed 2025-03-16 10:23:21 +08:00
Nathan
af0c802486 chore: local ai embed file 2025-03-16 10:22:58 +08:00
Lucas
33d518f383
chore: update unsplash client (#7543) 2025-03-14 21:52:44 +08:00
Nathan.fooo
1f9fe89f87
Merge pull request #7542 from AppFlowy-IO/replace_guide_url
chore: replace guide url
2025-03-14 21:52:06 +08:00
Nathan
5fef4f1d49 chore: replace guide url 2025-03-14 21:51:21 +08:00
Nathan.fooo
d6446872ee
Merge pull request #7541 from AppFlowy-IO/disable_chat_with_file
chore: disable chat with file
2025-03-14 20:55:34 +08:00
Nathan
5b5feb2515 chore: disable chat with file 2025-03-14 20:53:14 +08:00
Nathan.fooo
b1bca1b55b
Merge pull request #7538 from AppFlowy-IO/update_local_ai_log
Update local ai log
2025-03-14 18:32:36 +08:00
Nathan
4e1a70c7ac chore: update local ai crate 2025-03-14 17:15:12 +08:00
Nathan
54f25e4b91 Merge branch 'main' into update_local_ai_log 2025-03-14 14:29:35 +08:00
Richard Shiue
aa176f2c12
fix(mobile): image formats not shown (#7537) 2025-03-14 14:14:40 +08:00
Nathan
58895620c1 chore: update local ai logs 2025-03-14 13:23:41 +08:00
Nathan
e10aade895 chore: update local ai client 2025-03-14 11:36:30 +08:00
Richard Shiue
1fdd7c343b
chore: use tooltip instead of multi line for related questions (#7533) 2025-03-14 11:31:05 +08:00
Richard Shiue
e36b08cd14
chore: add tooltip for disabled select messages (#7536) 2025-03-14 11:10:52 +08:00
Richard Shiue
6ac9ad1cac
fix: continue writing edge case (#7535) 2025-03-14 11:10:40 +08:00
Richard Shiue
36bf90e81b
fix: stop generating in ai chat and writer (#7534)
* fix: stop generating shortcuts not working edge cases

* chore: add tooltip
2025-03-14 11:10:29 +08:00
Nathan
22b9acf386 chore: send local ai state 2025-03-13 19:51:42 +08:00
Nathan.fooo
e4e75acdac
Merge pull request #7527 from AppFlowy-IO/fix_windows_terminal
Fix windows terminal
2025-03-13 19:32:07 +08:00
Nathan
7996736592 chore: enable linux local ai 2025-03-13 17:27:55 +08:00
Nathan
3ad8f624cf chore: enable linux local ai 2025-03-13 17:03:49 +08:00
Nathan
0657aeb07d chore: set local ai default value 2025-03-13 17:02:32 +08:00
Nathan
723971e423 chore: fix editable 2025-03-13 16:44:41 +08:00
Nathan
86b67a1b65 chore: fix flashing window 2025-03-13 15:49:44 +08:00
Nathan
d94b4daa70 chore: fix flashing window 2025-03-13 15:29:58 +08:00
Lucas.Xu
ad62e85b3a Merge branch 'main' into fix_windows_terminal 2025-03-13 15:09:26 +08:00
Lucas
133eec8163
chore: update dsb_pub.pem on Windows (#7528) 2025-03-13 14:30:09 +08:00
Nathan
81bac5950c chore: try to fix windows flashing window issue 2025-03-13 14:01:41 +08:00
Lucas
651046ab68
fix: unable to paste iframe in editor (#7525) 2025-03-13 13:58:24 +08:00
Morn
9bd13ac29e
fix: the slash menu position sometimes is wrong (#7492) 2025-03-13 13:58:06 +08:00
Morn
c7d3d612ae
fix: emoji picker position error (#7497)
* fix: click an emoji should close the menu when using /emoji to insert an emoji into a doc

* fix: emoji picker position error

* fix: document emoji icon is clipped on Android
2025-03-13 13:57:58 +08:00
Morn
69dd2ab20f
feat: revamp toolbar UI (#7506)
* feat: revamp toolbar UI

* fix: integration test issues

* feat: add suggestions for toolbar

* feat: support dark mode

* chore: update editor dependency

* feat: add testing for suggestions

* chore: update editor dependency
2025-03-13 13:51:03 +08:00
Nathan
f0b8b00461 chore: force restart plugin 2025-03-13 13:45:01 +08:00
Nathan.fooo
2b8aaf1d46
Merge pull request #7522 from AppFlowy-IO/response_format_local_ai
chore: support response format
2025-03-13 13:08:52 +08:00
Nathan
ee69283a23 chore: support response format 2025-03-13 10:53:20 +08:00
Lucas
caaf5f7986
chore: remove deprecated pem files (#7521) 2025-03-13 10:27:11 +08:00
Richard Shiue
392964ffd2
chore: disable add messages to page when chat is empty (#7518) 2025-03-13 09:50:46 +08:00
Lucas
1f76412790
fix: nested list issue in quote/callout block (#7513) 2025-03-13 09:26:56 +08:00
Richard Shiue
555254e8fe
chore: add ai writer keyboard shortcuts (#7516) 2025-03-12 21:46:06 +08:00
Nathan.fooo
3aa55f83b1
chore: Merge pull request #7515 from AppFlowy-IO/local_ai_opti
chore: disable input when local ai is initializing
2025-03-12 21:20:48 +08:00
Nathan
d15a8a88a6 chore: disable input when local ai is initializing 2025-03-12 20:29:03 +08:00
Nathan
1f7ab9d22d chore: fix ios compile 2025-03-12 15:08:37 +08:00
Morn
44945b2912
fix: some slash menu issues (#7501)
* fix: slash menu unexpectedly overflows the screen

* fix: the style of no result doesn’t match the design

* fix: unexpected flashing effect on 2nd level menu item

* fix: can not back to last level through backspace
2025-03-12 14:10:41 +08:00
Lucas
8d50caa86e
fix: remove layout builder in quote block (#7508)
* fix: remove layout builder in quote block

* fix: quote block selection color

* fix: quote block and callout block background color issue

* fix: background color in callout block

* fix: quote block layout on mobile
2025-03-12 13:54:24 +08:00
Richard Shiue
b59eba76a6
fix: hide continue writing when document is empty (#7498)
* fix: hide continue writing when document is empty

* chore: code clean up and add documentation
2025-03-12 13:54:00 +08:00
Lucas
070cde9ecb
chore: bump version 0.8.7 (#7512) 2025-03-12 13:34:59 +08:00
FakhriAzzouz
1e81e4c68f
chore: update arabic translation
Arabic Translation Updates
2025-03-12 13:32:24 +08:00
Nathan.fooo
402ca7d765
Merge pull request #7511 from AppFlowy-IO/fix_ios_build
chore: fix ios build
2025-03-12 11:50:13 +08:00
Khor Shu Heng
d0ca7f311c
Merge pull request #7509 from khorshuheng/fix-recursion-trash-view
fix: prevent segfault due to infinite recursion in trash view
2025-03-12 11:42:40 +08:00
Nathan
e553627ee5 chore: fix ios build 2025-03-12 11:16:51 +08:00
khorshuheng
cbdac71025 fix: prevent segfault due to infinite recursion in trash view 2025-03-12 10:21:21 +08:00
Nathan.fooo
6f35ae9857
Merge pull request #7504 from richardshiue/chore/ollama-hide-formats
chore: hide predefined format section when using local ai
2025-03-12 09:57:44 +08:00
Nathan
20d64cc7ae chore: lint 2025-03-12 00:09:20 +08:00
Nathan
654e18aacf chore: remove local ai 2025-03-11 23:19:20 +08:00
Richard Shiue
75dd5c1d93 chore: regenerate 2025-03-11 22:18:44 +08:00
Richard Shiue
f8f9c3404a chore: show text options still 2025-03-11 22:05:30 +08:00
Richard Shiue
01e5817b24 chore: code cleanup 2025-03-11 17:49:51 +08:00
Richard Shiue
96608bd005 chore: hide predefined format section when using local ai 2025-03-11 17:06:55 +08:00
Nathan.fooo
57a5b38509
Merge pull request #7488 from AppFlowy-IO/ollama
feat: support Ollama
2025-03-11 13:53:35 +08:00
Nathan
83c53188e3 chore: clippy 2025-03-11 13:22:59 +08:00
Nathan
6ba7f93f69 chore: find plugin load 2025-03-11 13:14:47 +08:00
Nathan
702a486cce chore: find windows exe 2025-03-11 12:55:19 +08:00
Richard Shiue
eb0ed1ad86
fix: don't allow selection of text in related questions or loading (#7500) 2025-03-11 11:38:20 +08:00
Lucas
e264b3a5b8
Merge pull request #7490 from richardshiue/fix/callout-icon-in-markdown
fix: don't include icon while exporting callout to md
2025-03-11 10:38:46 +08:00
Richard Shiue
667d15c627
fix: ai writer gestures (#7499) 2025-03-11 10:31:08 +08:00
Nathan
bd06e1d559 chore: clippy 2025-03-11 09:32:20 +08:00
Lucas
459aca5291
Merge pull request #7496 from LucasXu0/ai_writer_position_improvement
feat: ensure the ai writer block visible when generating result
2025-03-11 09:31:24 +08:00
Nathan
e7cd90b6ab chore: update commit 2025-03-11 09:14:11 +08:00
Nathan
940db70447 chore: fix build 2025-03-11 00:27:55 +08:00
Nathan
59139ff323 chore: catch panic 2025-03-10 23:40:28 +08:00
Nathan
22fed1bfbc chore: load from env command 2025-03-10 22:48:09 +08:00
Lucas.Xu
5e593bd36e chore: update appflowy editor version 2025-03-10 21:08:28 +08:00
Lucas.Xu
ba1dfc6de4 feat: ensure the ai writer block visible when generating result 2025-03-10 19:18:06 +08:00
Lucas
c81f87dcdc
feat: make the columns block same width width the editor (#7493)
* feat: make the columns block same width width the editor

* chore: turn off column debug mode

* feat: add block selection container in outline block

* feat: use ratio instead of width in simple columns

* fix: document rules

* fix: turn off debug mode

* fix: update the existing columns block data
2025-03-10 18:13:15 +08:00
Nathan
a8b55ca3f0 chore: update prompt 2025-03-10 15:56:09 +08:00
Richard Shiue
0cefaf633c fix: fix test 2025-03-10 14:56:01 +08:00
Richard Shiue
ba4aebd005 chore: merge remote-tracking branch 'main' into this one 2025-03-10 13:36:08 +08:00
Lucas
7b32a92290
fix: callout block build error (#7491) 2025-03-10 13:04:29 +08:00
Richard Shiue
41b99209f1 fix: retain if is emoji 2025-03-10 12:10:59 +08:00
Morn
c1612fe298
feat: add custom icons for callout (#7449) 2025-03-10 11:46:55 +08:00
Lucas
e69a09d332
feat: support nested list in callout block and quote block (#7479)
* feat: support nested list in callout block

* chore: update pubspec.yml

* feat: add new quote block

* feat: support nested list in quote block

* feat: refacotr quote block

* feat: optimize quote block align

* feat: support nested list in quote block

* fix: icon and drag menu overlap

* chore: update appflowy editor version

* feat: support trailing action builder for plugin blocks

* chore: update appflowy editor version
2025-03-10 11:46:17 +08:00
Richard Shiue
4e0d9fdb0b fix: don't include icon while exporting callout to md 2025-03-10 11:08:22 +08:00
Nathan
8b2e769fca chore: ai setting ui 2025-03-10 10:24:55 +08:00
Nathan
d29a90a472 chore: init ai plugin on separate thread 2025-03-10 08:54:32 +08:00
Nathan
2e4beb0652 chore: enable windows 2025-03-10 00:35:52 +08:00
Nathan
addb041816 chore: update exe path 2025-03-10 00:25:19 +08:00
Nathan
4ff71b5dce chore: implement ollama 2025-03-09 23:32:42 +08:00
Richard Shiue
a0ae62d6f5
fix: consider simple table in exclude table types (#7478) 2025-03-07 12:32:27 +08:00
Lucas
ea18aa7551
chore: bump collab version 45239d2 (#7477) 2025-03-07 11:17:56 +08:00
Morn
68e7069e92
chore: bump version 0.8.6 & update changelog (#7473)
* chore: bump version 0.8.6

* chore: update changelog
2025-03-06 22:03:30 +08:00
Morn
556d929b67
fix: error caused by ScrollablePositionedList(#7460) (#7469)
* fix: error caused by ScrollablePositionedList

* chore: update appflowy_editor version
2025-03-06 18:45:27 +08:00
Lucas
7f3469a0f2
feat: use undo to revert the autotypograph (#7472) 2025-03-06 18:22:10 +08:00
Lucas
a062c4aadb
fix: bulleted list icon does not center in the columns (#7471) 2025-03-06 16:59:58 +08:00
Morn
3d3f81ad52
fix: complete the missing icons in the database (#7464)
* fix: complete the missing icons in the database

* fix: the toggle is slower than the actual change taken into effect
2025-03-06 16:04:54 +08:00
Lucas
fc0fb0b3d3
fix: update locked page button background color (#7470) 2025-03-06 16:02:56 +08:00
Richard Shiue
884586f0af
fix: ai writer issues (#7467)
* fix: disable ai writer in table

* fix: enable header row by default when converting from md

* chore: add title when continue writing

* chore: rewrite using predefined format

* fix: mouse & keyboard event still propagate

* chore: bump editor ref
2025-03-06 15:09:33 +08:00
Morn
f8c18afbcf
fix: improve the experience of using icon color picker (#7468) 2025-03-06 12:39:18 +08:00
Lucas
8046177d84
fix: simple columns issues (#7466)
* Revert "feat: use flutter_distrubutor to build linux and macos packages (#7392)"

This reverts commit 6dc45c9830.

* fix: linux link issue

* fix: outline doesn't work well in columns

* fix: cannot drag a block under a table that’s in the second column
2025-03-06 12:33:53 +08:00
Morn
8d8fc91391
fix: title position working incorrectly with document width setting (#7465) 2025-03-06 12:33:12 +08:00
Richard Shiue
796fda159e
chore: bump client api (#7463) 2025-03-06 09:59:53 +08:00
Nathan.fooo
4b2389dafd
chore: bump client api (#7455) 2025-03-05 10:23:28 +08:00
Richard Shiue
2dd7e5937f
fix: incorrect popover position (#7452)
* fix: incorrect popover position

* fix: tests

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-03-04 20:20:26 +08:00
Lucas
e9371029f3
chore: update changelog (#7451) 2025-03-04 17:17:57 +08:00
Richard Shiue
bbec60ff02
fix(flutter_desktop): page name overflow in search (#7450) 2025-03-04 17:17:42 +08:00
Lucas
3bf4f080c5
feat: auto calculate the column width when resizing (#7448)
* feat: calculate the column width auto

* fix: ai writer table issue
2025-03-04 16:34:26 +08:00
Richard Shiue
637c043f5b
fix: ai writer launch review 0.8.5 (#7445)
* fix: improve writing not working

* fix: show insert below and discard buttons even in overflow

* fix: incorrect predefined format initialization

* fix: generate image

* chore: multi-line related questions

* fix: add to undo history

* fix: disable keyboard service when using ai writer

* fix: disable drag nodes

* fix: strikethrough text after accepting

* fix: undo
2025-03-04 16:34:07 +08:00
Morn
9eed993421
feat: refactor databse styles (#7405)
* feat: refactor databse styles

* feat: support compact mode for databse

* feat: support dynamic height for board

* fix: add reference icon for database view in document

* feat: support data sync for database node in document

* fix: add hover effect in compact mode switcher

* fix: title of document not align correctly with a large screen

* fix: some launch review issues

* fix: auto hide the Hidden Groups unless the user clicks it to show

* fix: testing error

* chore: update board version

* chore: update database menu buttons

* fix: some launch review issues

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2025-03-04 11:29:38 +08:00
Lucas
aff720c1f1
fix: hide the improve writing button (#7443)
* fix: hide the improve writing button

* chore: update appflowy_editor version

* fix: simple column width

* Revert "fix: hide the improve writing button"

This reverts commit 815a28971c.
2025-03-04 11:29:24 +08:00
Lucas
655de30df5
fix: unable to delete the callout block when it's in the first line (#7442) 2025-03-03 16:40:41 +08:00
Richard Shiue
fe6217bd82
feat: ai writer block (#7406)
* feat: ai writer block

* test: fix integration tests

* chore: add continue writing to slash menu

* chore: focus issues during insertion

* fix: explain button position

* fix: gesture detection

* fix: insert below

* fix: undo

* chore: improve writing toolbar item

* chore: pass predefined format when using quick commands

* fix: continue writing in an empty document or at the beginning of a document

* fix: don't allow selecting text not in content

* fix: related question not following predefined format
2025-03-03 13:35:51 +08:00
Morn
eacd7b2503
fix: error display when showing SnackBar with dialog (#7440) 2025-03-03 13:15:50 +08:00
Lucas
2e17fb9dd3
fix: can't open the relation field in the linked database (#7441) 2025-03-03 13:14:29 +08:00
Morn
249543d64f
fix: using [[ to create subpage with error text (#7434) 2025-03-03 11:22:40 +08:00
Lucas
8ebd490260
feat: support scrollable columns block (#7429)
* feat: support scrollable columns block

* fix: simple columns block issues on mobile

* feat: hide drag menu when resizing the columns block
2025-03-03 11:22:13 +08:00
Morn
c0dfec8b34
fix: potential issues with displaying CircleAvatar (#7432) 2025-03-03 11:21:44 +08:00
Lucas
56a023c98a
fix: the locked hint is not visible when there's a cover (#7439) 2025-03-03 11:20:58 +08:00
Lucas
adcac881a7
fix: 0.8.5 launch review issues (#7430)
* chore: replace two columns with 2 columns

* fix: hide drag menu when the doc is locked

* feat: add placeholder when editing the paragraph

* fix: ingore tab shortcut in document title

* feat: forward the video block to link preview block
2025-03-03 09:57:36 +08:00
Lucas
f73342d902
fix: auto updater should not block the launch process (#7427) 2025-02-28 15:18:21 +08:00
FakhriAzzouz
45b0233c21
chore: update Arabic translations (#7361)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-27 13:14:48 +08:00
Lucas
db349519cf
chore: bump version 0.8.5 (#7421) 2025-02-27 13:13:49 +08:00
Lucas
c760a1b1fe
feat: support columns block in editor on desktop (#7402)
* feat: support columns block in editor

* feat: upgrade simple columns block

* fix: build error

* feat: add column width resizer

* fix: drag visual border

* fix: drag button position issue

* feat: add rule to check if the column is empty

* fix: flutter analyze

* feat: add document rules to delete the columns if its children are empty

* feat: support adding image in columns block

* feat: integrate block actions in columns block

* feat: support dragging to create a columns block

* feat: drag a block into an existing columns block

* feat: add delete columns and delete column rules

* feat: dragging the block to the left side of another block to create a columns block

* feat: support 2-4 columns block in slash menu

* chore: disable debug flag in columns block

* chore: update pubspec.yaml

* chore: update translations and icons

* fix: cloud integration test

* fix: integration test
2025-02-27 13:08:49 +08:00
Morn
c5fa9039b4
fix: add shortcut to create Inline Math Equation (#7401)
* fix: add shortcut to create Math Equation(#7331)

* chore: update code

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

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2025-02-26 10:06:28 +08:00
Richard Shiue
7eaafc52ce
fix: adjust other user message alignment (#7414) 2025-02-25 10:35:53 +08:00
Richard Shiue
63239893ab
chore(flutter): move other user message to end (#7413) 2025-02-24 15:14:26 +08:00
Annie
e9a1a1ced0
chore: Update README.md (#7411)
change appflowy.io to appflowy.com wherever possible
2025-02-23 13:46:13 +08:00
Lucas
6dc45c9830
feat: use flutter_distrubutor to build linux and macos packages (#7392)
* feat: use flutter_distrubutor to build linux packages

* feat: verify deb on Linux

* chore: update rpm deps

* chore: update codesign files

* chore: update rpm make_config.yaml

* chore: update release.yml

* chore: update release.yml

* chore: update feed url

* chore: rename AppFlowy to appflowy

* chore: update CHANGELOG.md (#7397)

* chore: create release path if not exist

* feat: support appimage

* Revert "feat: support appimage"

This reverts commit cb7dcf725c.

* fix: cp deb/rpm error

* feat: support appimage

* chore: add linux build script

* feat: add macos build script

* feat: update linux scripts

* chore: update linux scripts

* chore: update relesae script

* chore: update macos build scripts

* chore: rename macOS package name

* chore: add keychain in release.yaml

* chore: update macos build steps in release.yaml

* chore: update macos script desc

* chore: remove sudo

* feat: support tar.xz package type

* feat: support tar.xz package type

* chore: add fuse

---------

Co-authored-by: Morn <agedchen@gmail.com>
2025-02-21 17:39:13 +08:00
Morn
58f7659d55
fix: avatar displays error (#7403) 2025-02-21 14:28:24 +08:00
Morn
fd12d3a0b0
chore: update CHANGELOG.md (#7397) 2025-02-18 16:48:33 +08:00
Lucas
f25821e84d
chore: update auto updater feed url (#7396) 2025-02-18 16:14:13 +08:00
Morn
c0aa0e0509
fix: some launch review issues (#7379)
* fix: some launch review issues

* fix: some launch review issues

* fix: some launch review issues

* feat: add change button for icon uploader
2025-02-17 16:08:51 +08:00
Lucas
55fbb7522b
feat: switch ai mode on mobile (#7391)
* fix: the default page name should be empty when creating

* feat: switch ai model on mobile
2025-02-17 16:08:38 +08:00
Li, John Gen
15b4d496fd
chore: update translations (#7387)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-17 15:23:11 +08:00
Lucas
ad54ad0614
fix: the default page name should be empty when creating (#7389) 2025-02-17 15:13:25 +08:00
Lucas
b18bbd0e82
fix: auto updater issue on Windows (#7385) 2025-02-17 09:56:30 +08:00
Lucas
e028e45e93
fix: lock page issues (#7380)
* fix: unable to click the swith to lock/unlock page

* fix: add divider above delete button on mobile

* fix: enable lock/unlock page by tapping the lock icon

* chore: update translations

* fix: hide cursor when the page is locked

* fix: the inline databaes still can be edited if the document is locked

* fix: disable auto update checker

* chore: change my account to account & app
2025-02-14 17:02:25 +08:00
Lucas
0e7ac85f90
chore: add ai images label (#7376) 2025-02-13 14:56:16 +08:00
Lucas
8bb2541862
chore: upgrade version (#7374) 2025-02-13 13:57:37 +08:00
Lucas
133e61befd
fix: clear background color of header row & header column (#7373) 2025-02-13 13:21:02 +08:00
Morn
9e98680861
feat: support slash menu on mobile (#7368)
* feat: support slash menu on mobile

* feat: support at menu on mobile

* feat: support plus menu on mobile
2025-02-13 12:45:56 +08:00
Khor Shu Heng
b75fd673cd
chore: update collab version (#7372) 2025-02-13 12:45:45 +08:00
Lucas
4a7e20b3a5
fix: save image should not copy the image (#7370)
* fix: save image should not copy the image

* fix: unable to scroll the table in AI Chat on mobile
2025-02-12 17:35:59 +08:00
Morn
bbe746c564
feat: support upload svg as icon (#7270)
* feat: support upload svg as icon

* feat: support upload icon by pasting a link

* feat: delete remote images when remove custon icons

* chore: add testing for pasting image link as custon icon

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-02-12 15:08:50 +08:00
Reagan lee
189faa4def
chore: update translations (#7351)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-12 15:08:01 +08:00
Lucas
c1a8d89938
feat: lock page on mobile (#7366)
* feat: support lock button in view more actions

* feat: add lock page on mobile

* feat: disable actions in locked page

* feat: disable more actions in locked page

* feat: support locked grid on mobile

* feat: support locked board/calendar on mobile

* fix: exclude lock page button from AI Chat
2025-02-12 15:07:21 +08:00
Lucas
71ce9affbe
feat: lock page (#7353)
* feat: lock page

* feat: add pageLockStatus bloc

* feat: add lock status and unlock status in title bar

* feat: add loading lock status

* feat: disable moveTo, delete, rename, updateIcon operations if the page is locked

* fix: lock toast issue

* feat: support locked database

* feat: support locked grid

* feat: support locked title

* feat: support locked board

* feat: support locked calendar
2025-02-12 09:49:36 +08:00
Morn
552dba5abe
fix: support exporting more content to markdown (#7333)
* fix: support exporting to markdown with multiple images

* fix: support exporting to markdown with database

* fix: support exporting to markdown with date or reminder

* fix: support exporting to markdown with subpage and page reference

* chore: add some testing for markdown parser

* chore: add testing for exporting markdown with databse as csv
2025-02-11 21:46:02 +08:00
Richard Shiue
04e3246976
chore: rename predefined format enum variant (#7359) 2025-02-11 16:40:39 +08:00
Morn
5d73c3d194
fix: gallery not rendering in row page (#7349) 2025-02-11 14:03:49 +08:00
Richard Shiue
12d9a98831
chore: disable impeller on android (#7355) 2025-02-11 14:03:29 +08:00
Lucas
8f646a2843
feat: integrate version checker for Linux (#7346)
* feat: integrate auto_updater in macOS

* chore: update translations

* chore: bump auto_updater version

* feat: exclude linux platform in auto update task

* feat: support auto_updater on Linux

* chore: combine version checker and auto updater into same class
2025-02-10 15:07:53 +08:00
Lucas
f53e9d6549
feat: integrate auto_updater for macOS (#7328)
* feat: integrate auto_updater in macOS

* chore: update translations

* chore: bump auto_updater version

* feat: exclude linux platform in auto update task

* chore: disable auto updater

* fix: integration tests

* fix: integration tests
2025-02-10 09:20:24 +08:00
Richard Shiue
fc9c152553
fix(flutter_desktop): selection in AI chat going missing while scrolling (#7281) 2025-02-07 18:44:55 +08:00
Lucas
00cdee831d
chore: upgrade to Flutter 3.27.4 (#7230) 2025-02-07 18:17:46 +08:00
Morn
17ae05a623
fix: pasting a link on iOS results in incorrect behavior (#7326) 2025-02-07 14:49:50 +08:00
hasanbeder
8b1a03713b
feat(i18n): Add Turkish (tr-TR) language translation (#7329)
- Complete Turkish language translation for AppFlowy
- Covers all UI elements and user-facing strings
- Improves localization support for Turkish users
2025-02-07 09:58:15 +08:00
Richard Shiue
e4b57033b4
fix: improve calendar event button icon color and add confirm dialog (#7330)
* fix: improve calendar event button icon color and add confirm dialog

* test: update test
2025-02-06 22:46:22 +08:00
Richard Shiue
62a6fb8913
chore: optional response format (#7204)
* chore: optional response format

* chore: bump client api

* chore: code cleanup

* chore: bump client api

---------

Co-authored-by: Nathan <nathan@appflowy.io>
2025-02-06 18:10:23 +08:00
FakhriAzzouz
0d89e22ed2
chore(i18n): update ar-SA translations (#7312)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:54:16 +08:00
ArtemisOne
ff2aae213c
chore(i18n): update es-VE and ga-IE translations (#7320)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:53:24 +08:00
Richard Shiue
43e64d8219
test: fix image integration test (#7323) 2025-02-05 19:45:21 +08:00
Richard Shiue
6823fe5d24
fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:40:03 +08:00
Lucas
f683085618
fix: unable to upload file on Android devices (#7314) 2025-02-04 20:57:06 +08:00
Nathan.fooo
71a22dc466
chore: fix ai page user profile refresh (#7317) 2025-02-04 20:29:56 +08:00
Lucas
eb508a3ec9
fix: editor stuck on image loading loop when uploading image in row document (#7313)
* fix: editor stuck on image loading loop when uploading image in row document

* test: editor stuck on image loading loop when uploading image in row document
2025-02-04 14:05:57 +08:00
Nathan.fooo
aacd09d8e2
chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:08 +08:00
Peter Jose
25a27dfa81
chore(i18n): update id-ID translations (#7290)
- Translate 'themeMode' label
- Adjust text 'fontFamily'
- Update 'layoutDirection' translation
- Update 'textDirection' translation
2025-02-03 10:41:05 +08:00
Mohammad Mahdi Momeni
36349778e3
chore(i18n): update fa translations (#7292) 2025-02-03 10:05:34 +08:00
Nathan.fooo
9271d42db5
chore: fetch model list (#7306)
* chore: fetch model list
2025-02-01 23:48:32 +08:00
Richard Shiue
90d6e98b51
fix(flutter_mobile): drop focus on tap outside (#7274) 2025-01-24 09:23:52 +08:00
Lucas
dd6b285cd3
chore: update CHANGELOG.md (#7276) 2025-01-23 17:55:12 +08:00
FakhriAzzouz
8b0d5d76a2
New Arabic Translations (#7260)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-23 14:02:32 +08:00
Morn
85b69bde15
chore: cancel button not visible in [Reset to Default] for shortcuts(#7171) (#7257) 2025-01-23 13:58:48 +08:00
Morn
dd812d0501
chore: rename [font scale factor] to [scale factor] (#7267) 2025-01-23 13:58:09 +08:00
Richard Shiue
a79f825ff1
chore: restrict uploadable image types (#7269)
* chore: restrict image format

* chore: code cleanup
2025-01-23 10:51:15 +08:00
Lucas
862e5629e3
fix: v0.8.2 launch review issues (#7266)
* fix: unable to copy table from Apple Notes

* fix: disable pasting a table into another table

* fix: ignore the delete shortcut if the next node is a simple table node
2025-01-22 22:46:03 +08:00
Richard Shiue
c5a91e10df
chore: support pasting image links that have no file extension (#7262)
* test: speed up copypasta tests

* chore: strip away markdown syntax if message is image only

* chore: paste image urls with no file extension

* test: add integration test

* test: group tests

* chore: apply code suggestions to 3 files
2025-01-22 22:13:18 +08:00
Lucas
0b0e10baa8
feat: turn -> into to → in document (#7256)
* chore: bump version 0.8.2

* feat: turn -> into to → in document

* Revert "chore: bump version 0.8.2"

This reverts commit 45efd4d7d7.

* test: add shortcut tests
2025-01-22 15:05:22 +08:00
Nathan
ec18a3c443 chore: update docs 2025-01-22 09:47:06 +08:00
Nathan.fooo
a0867ed688
chore: return ai image error (#7263) 2025-01-22 09:42:24 +08:00
Nathan.fooo
cdf8e68ff2
Update plan desc (#7258)
* chore: update plan desc

* chore: update plan desc
2025-01-22 02:02:30 +08:00
Richard Shiue
af92303e38
fix: launch review issues 0.8.2 (#7261) 2025-01-21 22:17:53 +08:00
Lucas
504041d75a
chore: bump version 0.8.2 (#7255) 2025-01-21 13:31:40 +08:00
Lucas
06ab965413
chore: only enable document integrity check when enableDocumentInternalLog is on (#7251)
* chore: only enable document integrity check when enableDocumentInternalLog is on

* feat: copy divider and table from ChatGPT

* test: add copy from ChatGPT test

* feat: support copying link from keyboard clipboard
2025-01-21 13:29:31 +08:00
Morn
42bd4884fd
feat: support custom image icon (#7236)
* feat: supporting upload custom image as icon

* feat: support custom image icon on mobile

* chore: clean code
2025-01-21 11:58:45 +08:00
Lucas
0c057243bc
chore: bump client api (#7249)
* chore: bump client api

* chore: set default publish info to true
2025-01-21 09:18:25 +08:00
Lucas
8fdc6e9638
feat: support pasting table from notion, google docs and google sheet (#7247)
* feat: support pasting table from Notion

* test: add google docs / googles sheets table test

* fix: google docs test

* fix: paste table from notion test
2025-01-21 09:12:57 +08:00
Morn
187f7409ce
feat: enable customizing icons for database views (#7187)
* feat: enable set icon for databse tab

* chore: resolve conflicts

* feat: enable changing dabtabse icon on mobile
2025-01-20 17:55:01 +08:00
Morn
e3ce6e8b4b
fix: use WindowSizeManager to zoom on mobile (#7215) 2025-01-20 17:54:26 +08:00
Richard Shiue
cfe481759f
chore: bump table column width when saving chat message to page (#7243)
* chore: bump table column width when saving chat message to page

* test: add unit test
2025-01-20 13:28:02 +08:00
Richard Shiue
6bcef33d05
chore: trim markdown text (#7242)
* chore: trim markdown text

* chore: trim before saving
2025-01-20 11:47:03 +08:00
Lucas
aa8c9bad9f
fix: remove app from homepage url (#7233)
* fix: remove app from namespace url

* fix: banner issue on mobile

* Revert "fix: remove app from namespace url"

This reverts commit 4bd5f3f590.

* fix: remove app from namespace url
2025-01-17 20:55:22 +08:00
Richard Shiue
1723886f3a
feat: save messages as a new page (#7224) 2025-01-17 20:38:55 +08:00
FakhriAzzouz
63c7f7b6fe
chore: update ar-SA translations (#7216)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-17 10:08:09 +08:00
Lucas
f35dfaf525
fix: improve the document diff function to prevent partial ordering issues (#7217)
* fix: improve the document diff function to prevent partial ordering issues

* fix: improve the document diff function to prevent partial ordering issues

* fix: nested block padding issues

* fix: improve the document diff function to prevent partial ordering issues

* chore: update editor version

* test: add no diff test and update text diff test

* test: delete and insert text diff with different id

* test: insert single text / delete single text tests

* test: multiple delete and update diff

* test: multiple insert and update diff

* chore: revert cargo changes

* chore: remove unused code

* chore: optimize the code logic
2025-01-16 21:01:23 +08:00
Richard Shiue
eead2d20f5
fix: pass alwaysDistributeColumnWidths to simple table block (#7228) 2025-01-16 16:42:24 +08:00
Richard Shiue
f73c2540c6
chore: reorganize code (#7206) 2025-01-15 19:57:47 +08:00
Lucas
05c1924940
chore: update CHANGELOG.md (#7209)
* Revert "fix: disable deleting mutilple nodes in table"

This reverts commit 0507c39863.

* chore: bump version 0.8.1

* chore: remove unused tests
2025-01-14 21:04:00 +08:00
FakhriAzzouz
b2f3f902b2
chore: update ar-SA translations (#7210)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-14 19:59:11 +08:00
Richard Shiue
33fbba18ca
fix: message scrolling (#7207) 2025-01-14 16:37:13 +08:00
Nathan.fooo
cea3c69239
chore: update plan desc (#7208) 2025-01-14 16:29:42 +08:00
Morn
ff91c0a909
fix: correct the reference error of the icon widget (#7203) 2025-01-14 16:08:01 +08:00
Richard Shiue
d1efda4c50
fix: launch review 0.8.1 (#7185)
* fix: launch review

* chore: scroll to end upon sending new message

* chore: bump editor version

* chore: scroll to bottom after adding message

* chore: code reorg

* chore: bump editor version

* chore: bump editor ver

* chore: bump editor

* chore: bump editor

* fix: file block node insertion

* fix: do the same thing on image

* chore: update icons and translations
2025-01-14 12:35:51 +08:00
Morn
2b1d1ba2f4
chore: remove iconContent from IconsData (#7199) 2025-01-14 09:31:43 +08:00
Richard Shiue
9a237b5f18
chore: allow customize that forces distribute column widths (#7198)
* chore: allow customize that forces distribute column widths

* fix: missing intrinsic width

* fix: what am i doing
2025-01-13 18:47:45 +08:00
Lucas
fc21d1d245
fix: retry count should be clear if the value exceeds max retries (#7195)
* fix: retry count should be clear if the value exceeds max retries

* feat: add retry button when loading image failed
2025-01-13 17:17:05 +08:00
Morn
0c6d4df14b
fix: icons not supported in callout(#7192) (#7197) 2025-01-13 17:14:30 +08:00
Morn
8df5d8dcb4
fix: icons not display in search results (#7189)
* fix: icons not display in search results

* chore: clean code
2025-01-13 14:35:29 +08:00
Lucas
aa9f285f59
fix: search results were filtered accidentally (#7196) 2025-01-13 14:34:23 +08:00
Lucas
6173022a15
fix: 0.8.1 launch review issues (#7186)
* fix: Search result title appears empty

* chore: change "Type to search" to "Search or ask a question…"

* fix: the toolbar shouldn’t over the search panel

* feat: add hint text next to the web url

* test: the toolbar shouldn’t over the search panel

* test: search result title appears empty
2025-01-13 09:22:04 +08:00
Nathan.fooo
afb479607b
chore: bump client api (#7194) 2025-01-12 23:32:44 +08:00
Nathan.fooo
342c361184
chore: ai block with context (#7193)
* chore: ai block with context

* chore: fix test
2025-01-12 22:33:21 +08:00
Nathan.fooo
790d5612f7
chore: retry load image (#7179)
* chore: retry load image

* feat: support retry count and retry duration in network image

* chore: use loading builder in network image

* feat: support retry logic in network image

* feat: disable image menu when loading image

* chore: error prompt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-01-10 09:43:18 +08:00
Morn
99a4e330e8
feat: enable to reorder favorites (#7172) 2025-01-09 14:33:53 +08:00
Richard Shiue
c3b702849f
chore: remove error message when sending a new message (#7177) 2025-01-09 13:49:37 +08:00
Lucas
a49acd9220
chore: bump version 0.8.1 (#7178) 2025-01-09 12:04:42 +08:00
Lucas
dd3b4dc5fb
feat: resize table column width on mobile (#7174)
* feat: support resizing column width of table on mobile

* feat: support highlighting border when resizing

* test: support resizing column width of table on mobile
2025-01-09 11:37:43 +08:00
Morn
155817a0f1
fix: navigation bar issue on linked pages(#7111) (#7142) 2025-01-09 11:27:37 +08:00
Morn
89c4629ec2
feat: support auto text direction on mobile (#7092) (#7163) 2025-01-09 11:26:59 +08:00
Changho Park
6ca81b3a66
chore: update translations for ko-KR (#7161)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:11:05 +08:00
FakhriAzzouz
b278e14ffd
chore: update ar-SA translations (#7164)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:38 +08:00
dawn
cb6f7cb4b6
chore: update fr-CA translations (#7166)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:12 +08:00
mizou-soga
f94feb780a
chore: update ja-JP translations (#7167)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-09 10:07:01 +08:00
Lucas
0ef1b27ae5
fix: filter the search results by duplicate ids (#7173) 2025-01-08 19:33:24 +08:00
Lucas
64994b3336
chore: enable AI seach (#7169)
* chore: enable AI seach

* chore: remove unused code

* fix: replace the old web base url with the new one
2025-01-08 15:52:24 +08:00
Nathan.fooo
6966b303ff
chore: bump collab commit id (#7168)
* chore: bump collab commit id

* chore: bump collab version

* chore: bump collab version

* fix: disable deleting mutilple nodes in table

* chore: bump collab version

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-01-08 14:46:44 +08:00
Richard Shiue
ab8e01bbf7
feat(flutter): pre-defined response formats (#7128)
* feat: pre-defined response formats

* chore: adjust bottom sheet

* chore: rename and clean up enums

* chore: move all mobile input actions to the bottom

* chore: bump client-api

* chore: connect to API

* chore: apply suggestions from code review

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: code cleanup

* chore: code cleanup

* chore: update client-api

* chore: expand page

* chore: simplify logic for not displaying related questions

* chore: remove hover effect view icon in select sources

* chore: regenerate with different format

* chore: remove error messages when sending new one

* chore: code style

* chore: bump client api

* fix: image not displaying and hide editing options

* chore: don't fetch related questions for image only

* chore: fix clippy

* chore: don't add related questions on regenerate

* chore: bump editor

* fix: expand sidebar page

* chore: update client api

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: weidong fu <nathan@appflowy.io>
2025-01-08 10:43:03 +08:00
Richard Shiue
e25633636b
fix(flutter_mobile): linked grid cannot be displayed in document (#7162) 2025-01-07 17:56:17 +08:00
Lucas
d87a209c4a
fix: context menu should not auto show when collaborating (#7151)
* fix: context menu should not auto show when collaborating

* chore: update document_bloc.dart
2025-01-07 09:40:20 +08:00
Lucas
a33cf1f488
fix: filter out the space when opening new tab (#7152)
* fix: filter out the space when opening tab

* test: filter out the space when opening tab
2025-01-06 17:10:18 +08:00
Lucas
91236006d4
feat: custom share url, publish url and copy link to share (#7061)
* feat: custom share url, publish url and copy link to share

* chore: update translation

* feat: support base share domain

* feat: customize web url in login page

* feat: support customizing web url on mobile

* test: change web url test

* fix: cloud integration test

* fix: integration test

* fix: integration test
2025-01-06 16:05:05 +08:00
Richard Shiue
fd9baf7a06
chore: code cleanup (#7150) 2025-01-06 13:34:11 +08:00
Mathias Mogensen
ed646ccba2
chore: add loose restrictions to date time text field (#7024)
* chore: add loose restrictions to date time text field

* chore: remove date formatter
2025-01-05 21:02:29 +08:00
Lucas
7bc358d7ac
fix: simple table issues and locale issues (#7138)
* fix: can't make changes on row or column of table

* fix: fallback to en-US if the locale is invalid

* chore: remove unused code

* fix: simple table issues
2025-01-05 19:55:15 +08:00
Morn
14fc287071
fix: recent icon doesn't work in space icon (#7133) (#7137) 2025-01-05 19:54:59 +08:00
Morn
cdf7dced8b
fix: count title towards word count(#7042) (#7127) 2025-01-05 19:54:46 +08:00
Lucas
552c59218c
fix: convert false value in attributes to null (#7135) 2025-01-03 15:55:25 +08:00
Richard Shiue
c7e0e36902
feat(flutter_desktop): save ai message to page improvements (#7136)
* chore: expand current space in select sources or when saving to a page

* chore: hide non-document views while saving to page

* chore: add buttons when saving to page

* chore: adjust icon color

* chore: adjust translations

* chore: code cleanup
2025-01-03 14:01:19 +08:00
FakhriAzzouz
1f82c6682b
chore: update ar-SA translations (#7129)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-01-03 10:12:30 +08:00
Richard Shiue
21261ee813
feat(flutter_mobile): adjust bottom sheet quick action button style (#7121)
* feat: adjust bottom sheet quick action button style

* chore: remove final dividers
2025-01-03 10:11:17 +08:00
Morn
15deb8ea79
fix: icon picker issues on mobile (#7113)
* fix: error displaying in Page style

* fix: error displaying in Favorite/Recent page

* fix: complete the filter logic of icon picker

* fix: the color picker showed when tapping down

* fix: icons are not supported in subpage blocks

* chore: add some tests

* fix: recent icons not working for grid header icon
2025-01-03 10:04:14 +08:00
Kilu.He
f7f99a162e
chore: remove web app (#7126)
* feat: support rename workspace on web

* chore: remove web app from repo
2025-01-03 09:24:45 +08:00
Richard Shiue
c05f19edd2
feat(flutter_desktop): add button to expand side peek (#7118)
* chore: update sidebar top bar color

* feat(flutter_desktop): button to expand side peek

* Update frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: fix comment

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2025-01-02 15:51:40 +08:00
Richard Shiue
407e88b27f
chore: bump dependencies (#7123)
* chore: bump dependencies

* test: fix unit test

* fix: downgrade percent indicator

* chore: flutter analyze
2025-01-02 15:19:00 +08:00
Kilu.He
512113877b
feat: support create workspace on web (#7122) 2025-01-02 10:46:56 +08:00
Lucas
8f7cb50dd4
fix: simple table issues on mobile (#7115)
* fix: header row/column tap areas are too small on mobile

* test: header row/column tap areas are too small on mobile

* feat: enable auto scroll after inserting column or row

* fix: enter after emoji will create a softbreak on mobile

* fix: header row/column tap areas are too small on mobile

* fix: simple table alignment not work for item that wraps

* test: simple table alignment not work for item that wraps
2024-12-31 16:01:45 +08:00
Ahad Patel
7dedb84504
feat: i18n for duplicated field names (#6769)
* feat: i18n for duplicated field names

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 15:54:02 +08:00
Lucas
b2ca5c77f6
fix: simple tests on mobile (#7102)
* fix: simple tests on mobile

* fix: subpage block padding
2024-12-31 12:03:44 +08:00
Darío Hereñú
11d720465b
chore: update es-VE translations (#3645)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 11:04:37 +08:00
FakhriAzzouz
c2643bfb6c
chore: update ar-SA translations (#7099)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-12-31 11:02:16 +08:00
Morn
3d4d4cf709
test: add edit link menu test (#7097) 2024-12-31 09:46:13 +08:00
Ahad Patel
b965a5f3ae
feat: delete the previous image when the cover changes in local mode (#6368)
* remove unecessary images from localstorage

* feat: Add handler for deleting previous cover image on cover image change

* fix: add local image case for versions after 0.5.5

* fix: add try catch block and delete action to bottom of function

* chore: add test case for uploading and deleting image in localmode

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-31 09:36:14 +08:00
KD-MM2
73463cd7e3
chore(i18n): update vi-VN translations Fink 🐦 (#6290)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-12-30 18:38:34 +08:00
Richard Shiue
6158954d97
test: fix change server integeration test (#7096) 2024-12-30 18:10:55 +08:00
Kilu.He
3190eebf6e
fix: modified universal link (#7094) 2024-12-30 18:10:36 +08:00
Lucas
dfe994b341
feat: auto-dismiss collapsed handle on Android if no interaction occurs (#7088)
* feat: support auto-dismiss collapsed handle on Android

* fix: hit test area of collasepd handle is too big

* chore: upgrade appflowy_editor

* fix: simple table issues on mobile

* feat: highlight cell after insertion

* test: text color and cell background color test

* fix: sign_in_page_settings_test
2024-12-30 17:56:43 +08:00
Richard Shiue
92722d0922
fix(flutter_desktop): workspace menu ui issues (#7091)
* fix(flutter_desktop): remove log out and workspace option popovers conflict

* test: add integration test

* fix(flutter_desktop): workspace list scrollbar overlaps with list

* chore(flutter_desktop): fix padding around import from notion button

* chore(flutter_desktop): adjust popover conflict rules for workspace

* test: add integration tests

* chore(flutter_desktop): make the popoovers as barriers

* fix: regression from making the workspace item menu as barrier

* chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart

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

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-30 17:55:40 +08:00
Lucas
e04c1bdaec
chore: update changelog (#7095) 2024-12-30 17:53:55 +08:00
Richard Shiue
20b16cf174
feat: add toast messages for ai chat interactions (#7086) 2024-12-30 17:17:36 +08:00
Morn
5ffa27f545
fix: toolbar menu not showing beacase keyboard height is not updated in time (#7060) 2024-12-30 16:57:59 +08:00
Morn
00cdbe5a1c
fix: issues related to the emoji icon picker (#7063)
* fix: remove the scrolling conflict of the icon picker on macOS

* fix: the icon is not supported in sites tab

* feat: keep the icon panel open after click ramdom

* feat: the type of selector opened depends on the already set icon or emoji

* feat: the skin tone of the random emoji follows the selected skin ton

* fix: unit testing error
2024-12-30 16:42:14 +08:00
Richard Shiue
dffb865a66
chore(flutter_desktop): adjust toast style (#7083) 2024-12-30 16:35:41 +08:00
Kilu.He
5e581e912f
fix: hover controls position (#7093) 2024-12-30 15:51:58 +08:00
Kilu.He
256d015967
fix: add tip for database page (#7090) 2024-12-30 15:46:39 +08:00
Nathan.fooo
d9b3f3f6c6
chore: remove tarui (#7089)
* chore: remove tarui

* chore: remove tarui config
2024-12-30 15:41:23 +08:00
Nathan.fooo
09fa75f5ec
chore: remove crates (#7085)
* chore: remove crate

* chore: use rust 1.81

* chore: remove build flag

* fix: macos build error

* chore: upgrade super_clipboard

* chore: fix pb lint warning

* chore: fix clippy

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-12-30 14:25:05 +08:00
4073 changed files with 83390 additions and 180744 deletions

View file

@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
pull_request:
branches:
@ -19,8 +18,8 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64

View file

@ -25,8 +25,8 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.80.1"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64

View file

@ -7,7 +7,6 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
pull_request:
@ -16,12 +15,11 @@ on:
paths:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.80.1"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

View file

@ -6,8 +6,8 @@ on:
- "*"
env:
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.80.1"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
jobs:
create-release:
@ -338,7 +338,7 @@ jobs:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
os: ubuntu-22.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}

View file

@ -19,7 +19,7 @@ on:
env:
CARGO_TERM_COLOR: always
CLOUD_VERSION: 0.8.3-amd64
RUST_TOOLCHAIN: "1.80.1"
RUST_TOOLCHAIN: "1.81.0"
jobs:
ubuntu-job:

View file

@ -10,8 +10,8 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.80.1"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
jobs:
tests:

View file

@ -1,98 +0,0 @@
name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri2_ci.yaml"
- "frontend/rust-lib/**"
paths-ignore:
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.80.1"
CARGO_MAKE_VERSION: "0.36.6"
CI: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build-ubuntu:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Maximize build space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: install tauri deps tools
working-directory: frontend
run: |
cargo make appflowy-tauri-deps-tools
shell: bash
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
mkdir dist
pnpm install
cd src-tauri && cargo build
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint:tauri
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_web_app
args: "--debug"

View file

@ -1,111 +0,0 @@
name: Tauri-CI
on:
push:
branches:
- build/tauri
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.80.1"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
tauri-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
env:
CI: true
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_tauri/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force --locked duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force --locked cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
cargo make --cwd .. tauri_build
- name: frontend tests and linting
working-directory: frontend/appflowy_tauri
run: |
pnpm test
pnpm test:errors
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
args: "--debug"

View file

@ -1,152 +0,0 @@
name: Publish Tauri Release
on:
workflow_dispatch:
inputs:
branch:
description: "The branch to release"
required: true
default: "main"
version:
description: "The version to release"
required: true
default: "0.0.0"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.80.1"
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: windows-latest
args: "--verbose"
target: "windows-x86_64"
- platform: macos-latest
args: "--target x86_64-apple-darwin"
target: "macos-x86_64"
- platform: ubuntu-20.04
args: "--target x86_64-unknown-linux-gnu"
target: "linux-x86_64"
runs-on: ${{ matrix.settings.platform }}
env:
CI: true
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: Maximize build space (ubuntu only)
if: matrix.settings.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: install dependencies (windows only)
if: matrix.settings.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force --locked duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force --locked cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
cargo make --cwd .. tauri_build
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
CI: true
with:
args: ${{ matrix.settings.args }}
appVersion: ${{ github.event.inputs.version }}
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
- name: Upload EXE package(windows only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'windows-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.exe
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
- name: Upload DMG package(macos only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'macos-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.dmg
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
- name: Upload Deb package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.deb
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
- name: Upload AppImage package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.AppImage
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage

View file

@ -1,75 +0,0 @@
name: Web-CI
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
web-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: Run lint check
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build and analyze
working-directory: frontend/appflowy_web_app
run: |
pnpm run analyze >> analyze-size.txt
- name: Upload analyze-size.txt
uses: actions/upload-artifact@v4
with:
name: analyze-size.txt
path: frontend/appflowy_web_app/analyze-size.txt
retention-days: 30
- name: Upload stats.html
uses: actions/upload-artifact@v4
with:
name: stats.html
path: frontend/appflowy_web_app/dist/stats.html
retention-days: 30

View file

@ -1,65 +0,0 @@
name: Web Code Coverage
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
if: github.event.pull_request.draft != true
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
# Install pnpm dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v6
with:
working-directory: frontend/appflowy_web_app
component: true
build: pnpm run build
start: pnpm run start
browser: chrome
- name: Jest run
working-directory: frontend/appflowy_web_app
run: |
pnpm run test:unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
flags: appflowy_web_app
name: frontend/appflowy_web_app
fail_ci_if_error: true
verbose: true

View file

@ -1,19 +1,122 @@
# Release Notes
## Version 0.8.9 - 16/04/2025
### Desktop
#### New Features
- Supported pasting a link as a mention, providing a more condensed visualization of linked content
- Supported converting between link formats (e.g. transforming a mention into a bookmark)
- Improved the link editing experience with enhanced UX
- Added OTP (One-Time Password) support for sign-in authentication
- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet
#### Bug Fixes
- Fixed an issue where properties were not displaying in the row detail page
- Fixed a bug where Undo didn't work in the row detail page
- Fixed an issue where blocks didn't grow when the grid got bigger
- Fixed several bugs related to AI writers
### Mobile
#### New Features
- Added sign-in with OTP (One-Time Password)
#### Bug Fixes
- Fixed an issue where the slash menu sometimes failed to display
- Updated the mention page block to handle page selection with more context.
## Version 0.8.8 - 01/04/2025
### New Features
- Added support for selecting AI models in AI writer
- Revamped link menu in toolbar
- Added support for using ":" to add emojis in documents
- Passed the history of past AI prompts and responses to AI writer
### Bug Fixes
- Improved AI writer scrolling user experience
- Fixed issue where checklist items would disappear during reordering
- Fixed numbered lists generated by AI to maintain the same index as the input
## Version 0.8.7 - 18/03/2025
### New Features
- Made local AI free and integrated with Ollama
- Supported nested lists within callout and quote blocks
- Revamped the document's floating toolbar and added Turn Into
- Enabled custom icons in callout blocks
### Bug Fixes
- Fixed occasional incorrect positioning of the slash menu
- Improved AI Chat and AI Writers with various bug fixes
- Adjusted the columns block to match the width of the editor
- Fixed a potential segfault caused by infinite recursion in the trash view
- Resolved an issue where the first added cover might be invisible
- Fixed adding cover images via Unsplash
## Version 0.8.6 - 06/03/2025
### Bug Fixes
- Fix the incorrect title positioning when adjusting the document width setting
- Enhance the user experience of the icon color picker for smoother interactions
- Add missing icons to the database to ensure completeness and consistency
- Resolve the issue with links not functioning correctly on Linux systems
- Improve the outline feature to work seamlessly within columns
- Center the bulleted list icon within columns for better visual alignment
- Enable dragging blocks under tables in the second column to enhance flexibility
- Disable the AI writer feature within tables to prevent conflicts and improve usability
- Automatically enable the header row when converting content from Markdown to ensure proper formatting
- Use the "Undo" function to revert the auto-formatting
## Version 0.8.5 - 04/03/2025
### New Features
- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu
- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more
- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen
### Bug Fixes
- Fixed an issue where callout blocks couldnt be deleted when appearing as the first line in a document
- Fixed a bug preventing the relation field in databases from opening
- Fixed an issue where links in documents were unclickable on Linux
## Version 0.8.4 - 18/02/2025
### New Features
- Switch AI mode on mobile
- Support locking page
- Support uploading svg file as icon
- Support the slash, at, and plus menus on mobile
### Bug Fixes
- Gallery not rendering in row page
- Save image should not copy the image (mobile)
- Support exporting more content to markdown
## Version 0.8.2 - 23/01/2025
### New Features
- Customized database view icons
- Support for uploading images as custom icons
- Enabled selecting multiple AI messages to save into a document
- Added the ability to scale the app's display size on mobile
- Support for pasting image links without file extensions
### Bug Fixes
- Fixed an issue where pasting tables from other apps wasn't working
- Fixed homepage URL issues in Settings
- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page
## Version 0.8.1 - 14/01/2025
### New Features
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
### Bug Fixes
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
## Version 0.8.0 - 06/01/2025
### Bug Fixes
Fixed error displaying in the page style menu
Fixed filter logic in the icon picker
Fixed error displaying in the Favorite/Recent page
Fixed the color picker displaying when tapping down
Fixed icons not being supported in subpage blocks
Fixed recent icon functionality in the space icon menu
Fixed "Insert Below" not auto-scrolling the table
Fixed a to-do item with an emoji automatically creating a soft break
Fixed header row/column tap areas being too small
Fixed simple table alignment not working for items that wrap
Fixed web content reverting after removing the inline code format on desktop
Fixed inability to make changes to a row or column in the table when opening a new tab
Fixed changing the language to CKB-KU causing a gray screen on mobile
- Fixed error displaying in the page style menu
- Fixed filter logic in the icon picker
- Fixed error displaying in the Favorite/Recent page
- Fixed the color picker displaying when tapping down
- Fixed icons not being supported in subpage blocks
- Fixed recent icon functionality in the space icon menu
- Fixed "Insert Below" not auto-scrolling the table
- Fixed a to-do item with an emoji automatically creating a soft break
- Fixed header row/column tap areas being too small
- Fixed simple table alignment not working for items that wrap
- Fixed web content reverting after removing the inline code format on desktop
- Fixed inability to make changes to a row or column in the table when opening a new tab
- Fixed changing the language to CKB-KU causing a gray screen on mobile
## Version 0.7.9 - 30/12/2024
### New Features

View file

@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none">
<b>
<a href="https://www.appflowy.io">AppFlowy.IO</a><br>
<a href="https://www.appflowy.com">AppFlowy</a><br>
</b>
⭐️ The Open Source Alternative To Notion ⭐️ <br>
</h1>
@ -18,18 +18,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
</p>
<p align="center">
<a href="https://www.appflowy.io"><b>Website</b></a>
<a href="https://www.appflowy.com"><b>Website</b></a>
<a href="https://forum.appflowy.io/"><b>Forum</b></a>
<a href="https://discord.gg/9Q2xaN37tV"><b>Discord</b></a>
<a href="https://www.reddit.com/r/AppFlowy"><b>Reddit</b></a>
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</p>
<p align="center"><img src="https://appflowy.io/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
<br></br>
<p align="center" >
@ -48,7 +48,7 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
not supported
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
## Built With
@ -78,7 +78,7 @@ report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labe
## **Releases**
Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.
Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release.
## Contributing
@ -89,9 +89,7 @@ for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with
us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
the community, **Congratulations!** You are now an official contributor to AppFlowy.
## Translations 🌎🗺
@ -152,8 +150,8 @@ more information.
## Acknowledgments
Special thanks to these amazing projects which help power AppFlowy.IO:
Special thanks to these amazing projects which help power AppFlowy:
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)

View file

@ -4,7 +4,7 @@ workflows:
instance_type: mac_mini_m2
max_build_duration: 30
environment:
flutter: 3.22.3
flutter: 3.27.4
xcode: latest
cocoapods: default

View file

@ -1,140 +1,125 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// This task only builds the Dart code of AppFlowy.
// It supports both the desktop and mobile version.
"name": "AF: Build Dart Only",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "debug",
},
// uncomment the following line to testing performance.
// "flutterMode": "profile",
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
"RUST_LOG": "trace",
"RUST_BACKTRACE": "1"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds will:
// - call the clean task,
// - rebuild all the generated Files (including freeze and language files)
// - rebuild the the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-desktop: Debug Rust",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code
"type": "lldb",
"request": "launch",
"name": "AF-tauri: Debug backend",
"cargo": {
"args": [
"build",
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
"--no-default-features"
]
},
"preLaunchTask": "AF: Tauri UI Dev",
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// This task only builds the Dart code of AppFlowy.
// It supports both the desktop and mobile version.
"name": "AF: Build Dart Only",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "debug",
},
// uncomment the following line to testing performance.
// "flutterMode": "profile",
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
"RUST_LOG": "trace",
"RUST_BACKTRACE": "1"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
// This task builds will:
// - call the clean task,
// - rebuild all the generated Files (including freeze and language files)
// - rebuild the the Rust and Dart code of AppFlowy.
"name": "AF-desktop: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Clean + Rebuild All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-desktop: Debug Rust",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}"
// To launch the application directly, use the following configuration:
// "request": "launch",
// "program": "[YOUR_APPLICATION_PATH]",
},
]
}

View file

@ -245,51 +245,6 @@
"problemMatcher": [],
"detail": "appflowy_flutter"
},
{
"label": "AF: Tauri UI Build",
"type": "shell",
"command": "pnpm run build",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri UI Dev",
"type": "shell",
"isBackground": true,
"command": "pnpm sync:i18n && pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Tauri Clean",
"type": "shell",
"command": "cargo make tauri_clean",
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri Clean + Dev",
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Tauri Clean",
"AF: Tauri UI Dev"
],
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "AF: Tauri ESLint",
"type": "shell",
"command": "npx eslint --fix src",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
},
{
"label": "AF: Generate Env File",
"type": "shell",

View file

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.0"
APPFLOWY_VERSION = "0.8.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"

View file

@ -4,6 +4,7 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "packages/**/*.dart"
linter:
rules:

View file

@ -43,6 +43,8 @@
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
-->
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM9.25 3.75C9.25 4.44036 8.69036 5 8 5C7.30964 5 6.75 4.44036 6.75 3.75C6.75 3.05964 7.30964 2.5 8 2.5C8.69036 2.5 9.25 3.05964 9.25 3.75ZM12 8H9.41901L11.2047 13H9.081L8 9.97321L6.91901 13H4.79528L6.581 8H4V6H12V8Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 617 B

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
output: dist/
releases:
- name: dev
jobs:
- name: release-dev-linux-deb
package:
platform: linux
target: deb
- name: release-dev-linux-rpm
package:
platform: linux
target: rpm

View file

@ -0,0 +1,36 @@
-----BEGIN PUBLIC KEY-----
MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT
rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG
4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw
+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV
KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5
b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z
QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW
YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG
G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu
6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA
6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp
q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd
0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/
4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb
K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7
hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO
s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz
Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4
uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV
Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn
ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB
+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN
C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r
vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx
k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y
GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/
eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG
hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM
EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8
iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI
7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb
w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf
1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P
Y29SB4jvwqls268rP0cWqy4WXwlVwuc=
-----END PUBLIC KEY-----

View file

@ -23,24 +23,24 @@ void main() {
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
// Is expanded by default
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
// Collapse hidden groups
await tester.tap(collapseFinder);
await tester.pumpAndSettle();
// Is collapsed
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
// Expand hidden groups
// Collapse hidden groups
await tester.tap(expandFinder);
await tester.pumpAndSettle();
// Is expanded
// Is collapsed
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
// Expand hidden groups
await tester.tap(collapseFinder);
await tester.pumpAndSettle();
// Is expanded
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
});
testWidgets('hide first group, and show it again', (tester) async {
@ -48,6 +48,9 @@ void main() {
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
await tester.tapButton(expandFinder);
// Tap the options of the first group
final optionsFinder = find
.descendant(

View file

@ -1,5 +1,6 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'database/database_test_runner.dart' as database_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
@ -28,4 +29,7 @@ Future<void> main() async {
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
// database
database_test_runner.main();
}

View file

@ -15,7 +15,6 @@ void main() {
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapContinousAnotherWay();
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
@ -31,12 +30,6 @@ void main() {
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google

View file

@ -0,0 +1,80 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// copy link to block
group('database image:', () {
testWidgets('insert image', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open the first row detail page and upload an image
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Grid,
pageName: 'database image',
);
await tester.openFirstRowDetailPage();
// insert an image block
{
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
}
// upload an image
{
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(
paths: [imagePath],
);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
// remove the temp file
file.deleteSync();
}
});
});
}

View file

@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'database_image_test.dart' as database_image_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_image_test.main();
}

View file

@ -33,7 +33,7 @@ void main() {
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
);
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
expect(find.byType(AiWriterBlockComponent), findsOneWidget);
// switch to another page
await tester.openPage(Constants.gettingStartedPageName);
@ -41,7 +41,7 @@ void main() {
await tester.openPage(pageName);
// expect the ai writer block is not in the document
expect(find.byType(AIWriterBlockComponent), findsNothing);
expect(find.byType(AiWriterBlockComponent), findsNothing);
});
});
}

View file

@ -57,7 +57,7 @@ void main() {
// move the checkbox to the child of the block at path [9]
await tester.editor.dragBlock(
[10],
const Offset(80, -30),
const Offset(120, -20),
);
// wait for the move animation to complete

View file

@ -45,7 +45,7 @@ void main() {
);
final icons = find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
);
expect(icons, findsOneWidget);
await tester.tapIcon(EmojiIconData.icon(firstIcon));
@ -54,7 +54,7 @@ void main() {
final spaceIcon = find.descendant(
of: spaceHeader,
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
),
);
expect(spaceIcon, findsOneWidget);

View file

@ -57,12 +57,6 @@ void main() {
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeGoogleLoginButton();

View file

@ -1,7 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/loading.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:easy_localization/easy_localization.dart';

View file

@ -54,7 +54,7 @@ void main() {
);
final shareValues = plainText!
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
.split('/');
final workspaceId = shareValues[0];
expect(workspaceId, isNotEmpty);

View file

@ -1,5 +1,6 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/shared/loading.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
@ -71,5 +72,16 @@ void main() {
expect(find.byType(FlowyTab), findsNothing);
});
testWidgets('the space view should not be opened', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
expect(find.byType(AppFlowyEditorPage), findsNothing);
expect(find.text('Blank page'), findsOne);
});
});
}

View file

@ -120,6 +120,10 @@ void main() {
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
if (pageItem.evaluate().isEmpty) {
return;
}
expect(pageItem, findsOneWidget);
// comment it out because it's not allowed to update the namespace in free plan
@ -249,7 +253,7 @@ More actions for published page:
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.sites);
// wait the backend return the sites data
await tester.wait(1000);
await tester.wait(2000);
// check if the page is published in sites page
final pageItem = find.byWidgetPredicate(
@ -257,6 +261,10 @@ More actions for published page:
widget is PublishedViewItem &&
widget.publishInfoView.view.name == pageName,
);
if (pageItem.evaluate().isEmpty) {
return;
}
expect(pageItem, findsOneWidget);
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());

View file

@ -1,6 +1,15 @@
import 'dart:convert';
import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -8,7 +17,9 @@ import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
});
group('Folder Search', () {
testWidgets('Search for views', (tester) async {
@ -33,21 +44,106 @@ void main() {
await tester.pumpAndSettle(const Duration(milliseconds: 200));
// Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna)
expect(find.byType(SearchResultTile), findsNWidgets(2));
expect(find.byType(SearchResultCell), findsNWidgets(2));
// The score should be higher for "ViewOna" thus it should be shown first
final secondDocumentWidget = tester
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
expect(secondDocumentWidget.result.data, secondDocument);
.widget(find.byType(SearchResultCell).first) as SearchResultCell;
expect(secondDocumentWidget.item.displayName, secondDocument);
// Change search to "ViewOne"
await tester.enterText(searchFieldFinder, firstDocument);
await tester.pumpAndSettle(const Duration(seconds: 1));
// The score should be higher for "ViewOne" thus it should be shown first
final firstDocumentWidget = tester
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
expect(firstDocumentWidget.result.data, firstDocument);
final firstDocumentWidget = tester.widget(
find.byType(SearchResultCell).first,
) as SearchResultCell;
expect(firstDocumentWidget.item.displayName, firstDocument);
});
testWidgets('Displaying icons in search results', (tester) async {
final randomValue = Random().nextInt(10000) + 10000;
final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final emojiIconData = await tester.loadIcon();
/// create two pages
for (final pageName in pageNames) {
await tester.createNewPageWithNameUnderParent(name: pageName);
await tester.updatePageIconInTitleBarByName(
name: pageName,
layout: ViewLayoutPB.Document,
icon: emojiIconData,
);
}
await tester.toggleCommandPalette();
/// search for `Page`
final searchFieldFinder = find.descendant(
of: find.byType(SearchField),
matching: find.byType(FlowyTextField),
);
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.byType(SearchResultCell), findsNWidgets(2));
/// check results
final svgs = find.descendant(
of: find.byType(SearchResultCell),
matching: find.byType(FlowySvg),
);
expect(svgs, findsNWidgets(2));
final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,
lastSvg = svgs.last.evaluate().first.widget as FlowySvg;
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
/// icon displayed correctly
expect(firstSvg.svgString, iconData.svgString);
expect(lastSvg.svgString, iconData.svgString);
testWidgets('select the content in document and search', (tester) async {
const firstDocument = ''; // empty document
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: firstDocument);
await tester.editor.updateSelection(
Selection(
start: Position(
path: [0],
),
end: Position(
path: [0],
offset: 10,
),
),
);
await tester.pumpAndSettle();
expect(
find.byType(FloatingToolbar),
findsOneWidget,
);
await tester.toggleCommandPalette();
expect(find.byType(CommandPaletteModal), findsOneWidget);
expect(
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
findsOneWidget,
);
expect(
find.text(firstDocument),
findsOneWidget,
);
});
});
});
}

View file

@ -1,5 +1,5 @@
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -27,11 +27,12 @@ void main() {
expect(find.byType(RecentViewsList), findsOneWidget);
// Expect three recent history items
expect(find.byType(RecentViewTile), findsNWidgets(3));
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
// Expect the first item to be the last viewed document
final firstDocumentWidget =
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
tester.widget(find.byType(SearchRecentViewCell).first)
as SearchRecentViewCell;
expect(firstDocumentWidget.view.name, secondDocument);
});
});

View file

@ -15,6 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
@ -29,6 +30,11 @@ void main() {
await tester.tapHidePropertyButton();
tester.noFieldWithName('New field 1');
// create another field, New field 1 to be hidden still
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
tester.noFieldWithName('New field 1');
// go back to inline database view, expect field to be shown
await tester.tapTabBarLinkedViewByViewName('Untitled');
tester.findFieldWithName('New field 1');
@ -60,5 +66,40 @@ void main() {
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1");
});
testWidgets('field cell width', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a database and add a linked database view
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// check the width of the field
expect(tester.getFieldWidth('New field 1'), 150);
// change the width of the field
await tester.changeFieldWidth('New field 1', 200);
expect(tester.getFieldWidth('New field 1'), 205);
// create another field, New field 1 to be same width
await tester.tapNewPropertyButton();
await tester.dismissFieldEditor();
expect(tester.getFieldWidth('New field 1'), 205);
// go back to inline database view, expect New field 1 to be 150px
await tester.tapTabBarLinkedViewByViewName('Untitled');
expect(tester.getFieldWidth('New field 1'), 150);
// go back to linked database view, expect New field 1 to be 205px
await tester.tapTabBarLinkedViewByViewName('Grid');
expect(tester.getFieldWidth('New field 1'), 205);
});
});
}

View file

@ -0,0 +1,190 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
testWidgets('change icon', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
const pageName = 'Database';
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Grid,
name: pageName,
);
/// create board
final addButton = find.byType(AddDatabaseViewButton);
await tester.tapButton(addButton);
await tester.tapButton(
find.text(
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',
findRichText: true,
),
);
/// create calendar
await tester.tapButton(addButton);
await tester.tapButton(
find.text(
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',
findRichText: true,
),
);
final databaseTabBarItem = find.byType(DatabaseTabBarItem);
expect(databaseTabBarItem, findsNWidgets(3));
final gridItem = databaseTabBarItem.first,
boardItem = databaseTabBarItem.at(1),
calendarItem = databaseTabBarItem.last;
/// change the icon of grid
/// the first tapping is to select specific item
/// the second tapping is to show the menu
await tester.tapButton(gridItem);
await tester.tapButton(gridItem);
/// change icon
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final gridIcon = find.descendant(
of: gridItem,
matching: find.byType(RawEmojiIconWidget),
);
final gridIconWidget =
gridIcon.evaluate().first.widget as RawEmojiIconWidget;
final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));
final gridIconsData =
IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));
expect(gridIconsData.iconName, iconsData.iconName);
/// change the icon of board
await tester.tapButton(boardItem);
await tester.tapButton(boardItem);
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final boardIcon = find.descendant(
of: boardItem,
matching: find.byType(RawEmojiIconWidget),
);
final boardIconWidget =
boardIcon.evaluate().first.widget as RawEmojiIconWidget;
final boardIconsData =
IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));
expect(boardIconsData.iconName, iconsData.iconName);
/// change the icon of calendar
await tester.tapButton(calendarItem);
await tester.tapButton(calendarItem);
await tester
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
await tester.tapIcon(iconData, enableColor: false);
final calendarIcon = find.descendant(
of: calendarItem,
matching: find.byType(RawEmojiIconWidget),
);
final calendarIconWidget =
calendarIcon.evaluate().first.widget as RawEmojiIconWidget;
final calendarIconsData =
IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));
expect(calendarIconsData.iconName, iconsData.iconName);
});
testWidgets('change database icon from sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final iconData = await tester.loadIcon();
final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';
const pageName = 'Database';
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Grid,
name: pageName,
);
final viewItem = find.descendant(
of: find.byType(SidebarFolder),
matching: find.byWidgetPredicate(
(w) => w is ViewItem && w.view.name == pageName,
),
);
/// change icon to emoji
await tester.tapButton(
find.descendant(
of: viewItem,
matching: find.byType(FlowySvg),
),
);
await tester.tapEmoji(emoji);
final iconWidget = find.descendant(
of: viewItem,
matching: find.byType(RawEmojiIconWidget),
);
expect(
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
emoji,
);
/// the icon will not be displayed in database item
Finder databaseIcon = find.descendant(
of: find.byType(DatabaseTabBarItem),
matching: find.byType(FlowySvg),
);
expect(
(databaseIcon.evaluate().first.widget as FlowySvg).svg,
FlowySvgs.icon_grid_s,
);
/// change emoji to icon
await tester.tapButton(iconWidget);
await tester.tapIcon(iconData);
expect(
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
iconData.emoji,
);
databaseIcon = find.descendant(
of: find.byType(DatabaseTabBarItem),
matching: find.byType(RawEmojiIconWidget),
);
final databaseIconWidget =
databaseIcon.evaluate().first.widget as RawEmojiIconWidget;
final databaseIconsData =
IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));
expect(icon.svgString, databaseIconsData.svgString);
expect(icon.color, isNotEmpty);
expect(icon.color, databaseIconsData.color);
/// the icon in database item should not show the color
expect(databaseIconWidget.enableColor, false);
});
}

View file

@ -1,5 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -73,5 +78,37 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('insert grid in column', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create page and show slash menu
await tester.createNewPageWithNameUnderParent(name: 'test page');
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
/// create a column
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_twoColumns.tr(),
);
final actionList = find.byType(BlockActionList);
expect(actionList, findsNWidgets(2));
final position = tester.getCenter(actionList.last);
/// tap the second child of column
await tester.tapAt(position.copyWith(dx: position.dx + 50));
/// create a grid
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_grid.tr(),
);
final grid = find.byType(GridPageContent);
expect(grid, findsOneWidget);
});
});
}

View file

@ -27,8 +27,9 @@ void main() {
await tester.pumpAndSettle();
// click the align center
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
await tester
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m);
// expect to see the align center
final editorState = tester.editor.getCurrentEditorState();
@ -36,13 +37,15 @@ void main() {
expect(first.attributes[blockComponentAlign], 'center');
// click the align right
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
await tester
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m);
expect(first.attributes[blockComponentAlign], 'right');
// click the align left
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);
await tester
.tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m);
expect(first.attributes[blockComponentAlign], 'left');
});
@ -75,7 +78,7 @@ void main() {
[
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyE,
LogicalKeyboardKey.keyC,
],
tester: tester,
withKeyUp: true,

View file

@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
testWidgets('callout with emoji icon picker', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final emojiIconData = await tester.loadIcon();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
/// create callout
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_callout.tr(),
);
/// select an icon
final emojiPickerButton = find.descendant(
of: find.byType(CalloutBlockComponentWidget),
matching: find.byType(EmojiPickerButton),
);
await tester.tapButton(emojiPickerButton);
await tester.tapIcon(emojiIconData);
/// verification results
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
final iconWidget = find
.descendant(
of: emojiPickerButton,
matching: find.byType(IconWidget),
)
.evaluate()
.first
.widget as IconWidget;
final iconWidgetData = iconWidget.iconsData;
expect(iconWidgetData.svgString, iconData.svgString);
expect(iconWidgetData.iconName, iconData.iconName);
expect(iconWidgetData.groupName, iconData.groupName);
});
}

View file

@ -13,6 +13,8 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final finder = find.text(gettingStarted, findRichText: true);
await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));
// create a new document
const pageName = 'Test Document';

View file

@ -0,0 +1,453 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const avaliableLink = 'https://appflowy.io/',
unavailableLink = 'www.thereIsNoting.com';
Future<void> preparePage(WidgetTester tester, {String? pageName}) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(name: pageName);
await tester.editor.tapLineOfEditorAt(0);
}
Future<void> pasteLink(WidgetTester tester, String link) async {
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: link));
/// paste the link
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.pumpAndSettle(Duration(seconds: 1));
}
Future<void> pasteAs(
WidgetTester tester,
String link,
PasteMenuType type, {
Duration waitTime = const Duration(milliseconds: 500),
}) async {
await pasteLink(tester, link);
final convertToMentionButton = find.text(type.title);
await tester.tapButton(convertToMentionButton);
await tester.pumpAndSettle(waitTime);
}
void checkUrl(Node node, String link) {
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{
'insert': link,
'attributes': {'href': link},
}
]);
}
void checkMention(Node node, String link) {
final delta = node.delta!;
final insert = (delta.first as TextInsert).text;
final attributes = delta.first.attributes;
expect(insert, MentionBlockKeys.mentionChar);
final mention =
attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;
expect(mention[MentionBlockKeys.type], MentionType.externalLink.name);
expect(mention[MentionBlockKeys.url], avaliableLink);
}
void checkBookmark(Node node, String link) {
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], link);
}
void checkEmbed(Node node, String link) {
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed);
expect(node.attributes[LinkPreviewBlockKeys.url], link);
}
group('Paste as URL', () {
Future<void> pasteAndTurnInto(
WidgetTester tester,
String link,
String title,
) async {
await pasteLink(tester, link);
final convertToLinkButton = find
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
await tester.tapButton(convertToLinkButton);
/// hover link and turn into mention
await tester.hoverOnWidget(
find.byType(LinkHoverTrigger),
onHover: () async {
final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(turnintoButton);
final convertToButton = find.text(title);
await tester.tapButton(convertToButton);
await tester.pumpAndSettle(Duration(seconds: 1));
},
);
}
testWidgets('paste a link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteLink(tester, link);
final convertToLinkButton = find
.text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());
await tester.tapButton(convertToLinkButton);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link and turn into mention', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toMention.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link and turn into bookmark', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toBookmark.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste a link and turn into embed', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAndTurnInto(
tester,
link,
LinkConvertMenuCommand.toEmbed.title,
);
/// check metion values
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
});
group('Paste as Mention', () {
Future<void> pasteAsMention(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.mention);
String getMentionLink(Node node) {
final insert = node.delta?.first as TextInsert?;
final mention = insert?.attributes?[MentionBlockKeys.mention]
as Map<String, dynamic>?;
return mention?[MentionBlockKeys.url] ?? '';
}
Future<void> hoverMentionAndClick(
WidgetTester tester,
String command,
) async {
final mentionLink = find.byType(MentionLinkBlock);
expect(mentionLink, findsOneWidget);
await tester.hoverOnWidget(
mentionLink,
onHover: () async {
final errorPreview = find.byType(MentionLinkErrorPreview);
expect(errorPreview, findsOneWidget);
final convertButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(convertButton);
final menuButton = find.text(command);
await tester.tapButton(menuButton);
},
);
}
testWidgets('paste a link as mention', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste as mention and copy link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
final mentionLink = find.byType(MentionLinkBlock);
expect(mentionLink, findsOneWidget);
await tester.hoverOnWidget(
mentionLink,
onHover: () async {
final preview = find.byType(MentionLinkPreview);
if (!preview.hasFound) {
final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
await tester.tapButton(copyButton);
} else {
final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
await tester.tapButton(moreOptionButton);
final copyButton =
find.text(MentionLinktMenuCommand.copyLink.title);
await tester.tapButton(copyButton);
}
},
);
final clipboardContent = await getIt<ClipboardService>().getData();
expect(clipboardContent.plainText, link);
});
testWidgets('paste as error mention and turninto url', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toURL.title,
);
node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste as error mention and turninto embed', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toEmbed.title,
);
node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste as error mention and turninto bookmark', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.toBookmark.title,
);
node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste as error mention and remove link', (tester) async {
String link = unavailableLink;
await preparePage(tester);
await pasteAsMention(tester, link);
Node node = tester.editor.getNodeAtPath([0]);
link = getMentionLink(node);
await hoverMentionAndClick(
tester,
MentionLinktErrorMenuCommand.removeLink.title,
);
node = tester.editor.getNodeAtPath([0]);
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{'insert': link},
]);
});
});
group('Paste as Bookmark', () {
Future<void> pasteAsBookmark(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.bookmark);
Future<void> hoverAndClick(
WidgetTester tester,
LinkPreviewMenuCommand command,
) async {
final bookmark = find.byType(CustomLinkPreviewBlockComponent);
expect(bookmark, findsOneWidget);
await tester.hoverOnWidget(
bookmark,
onHover: () async {
final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);
await tester.tapButton(menuButton);
final commandButton = find.text(command.title);
await tester.tapButton(commandButton);
},
);
}
testWidgets('paste a link as bookmark', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
testWidgets('paste a link as bookmark and convert to mention',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link as bookmark and convert to url', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link as bookmark and convert to embed',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed);
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste a link as bookmark and copy link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink);
final clipboardContent = await getIt<ClipboardService>().getData();
expect(clipboardContent.plainText, link);
});
testWidgets('paste a link as bookmark and replace link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.replace);
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
await tester.enterText(find.byType(TextFormField), unavailableLink);
await tester.tapButton(find.text(LocaleKeys.button_replace.tr()));
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, unavailableLink);
});
testWidgets('paste a link as bookmark and remove link', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsBookmark(tester, link);
await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink);
final node = tester.editor.getNodeAtPath([0]);
expect(node.type, ParagraphBlockKeys.type);
expect(node.delta!.toJson(), [
{'insert': link},
]);
});
});
group('Paste as Embed', () {
Future<void> pasteAsEmbed(WidgetTester tester, String link) =>
pasteAs(tester, link, PasteMenuType.embed);
Future<void> hoverAndConvert(
WidgetTester tester,
LinkEmbedConvertCommand command,
) async {
final embed = find.byType(LinkEmbedBlockComponent);
expect(embed, findsOneWidget);
await tester.hoverOnWidget(
embed,
onHover: () async {
final menuButton = find.byFlowySvg(FlowySvgs.turninto_m);
await tester.tapButton(menuButton);
final commandButton = find.text(command.title);
await tester.tapButton(commandButton);
},
);
}
testWidgets('paste a link as embed', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
final node = tester.editor.getNodeAtPath([0]);
checkEmbed(node, link);
});
testWidgets('paste a link as bookmark and convert to mention',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);
final node = tester.editor.getNodeAtPath([0]);
checkMention(node, link);
});
testWidgets('paste a link as bookmark and convert to url', (tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);
final node = tester.editor.getNodeAtPath([0]);
checkUrl(node, link);
});
testWidgets('paste a link as bookmark and convert to bookmark',
(tester) async {
final link = avaliableLink;
await preparePage(tester);
await pasteAsEmbed(tester, link);
await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);
final node = tester.editor.getNodeAtPath([0]);
checkBookmark(node, link);
});
});
}

View file

@ -1,4 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -31,4 +37,104 @@ void main() {
expect(pageFinder, findsNWidgets(1));
});
});
testWidgets('count title towards word count', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
Finder title = tester.editor.findDocumentTitle('');
await tester.openMoreViewActions();
final viewMetaInfo = find.byType(ViewMetaInfo);
expect(viewMetaInfo, findsOneWidget);
ViewMetaInfo viewMetaInfoWidget =
viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
Counters titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, 0);
expect(titleCounter.wordCount, 0);
/// input [str1] within title
const str1 = 'Hello',
str2 = '$str1 AppFlowy',
str3 = '$str2!',
str4 = 'Hello world';
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str1);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str1.length);
expect(titleCounter.wordCount, 1);
/// input [str2] within title
title = tester.editor.findDocumentTitle(str1);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str2);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str2.length);
expect(titleCounter.wordCount, 2);
/// input [str3] within title
title = tester.editor.findDocumentTitle(str2);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.tapButton(title);
await tester.enterText(title, str3);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
expect(titleCounter.charCount, str3.length);
expect(titleCounter.wordCount, 2);
/// input [str4] within document
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
await tester.editor
.updateSelection(Selection.collapsed(Position(path: [0])));
await tester.pumpAndSettle();
await tester.editor
.getCurrentEditorState()
.insertTextAtCurrentSelection(str4);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.openMoreViewActions();
final texts =
find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));
expect(texts, findsNWidgets(3));
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
titleCounter = viewMetaInfoWidget.titleCounters!;
final Counters documentCounters = viewMetaInfoWidget.documentCounters!;
final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,
charCounter = texts.evaluate().elementAt(1).widget as FlowyText;
final numberFormat = NumberFormat();
expect(
wordCounter.text,
LocaleKeys.moreAction_wordCount.tr(
args: [
numberFormat
.format(titleCounter.wordCount + documentCounters.wordCount)
.toString(),
],
),
);
expect(
charCounter.text,
LocaleKeys.moreAction_charCount.tr(
args: [
numberFormat
.format(
titleCounter.charCount + documentCounters.charCount,
)
.toString(),
],
),
);
});
}

View file

@ -76,13 +76,12 @@ void main() {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
LocaleKeys.editor_bulletedListShortForm.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
LocaleKeys.editor_numberedListShortForm.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};
@ -117,13 +116,12 @@ void main() {
LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,
LocaleKeys.document_slashMenu_name_bulletedList.tr():
LocaleKeys.editor_bulletedListShortForm.tr():
BulletedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_numberedList.tr():
LocaleKeys.editor_numberedListShortForm.tr():
NumberedListBlockKeys.type,
LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,
LocaleKeys.document_slashMenu_name_todoList.tr():
TodoListBlockKeys.type,
LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,
LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,
LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,
};

View file

@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -47,5 +48,41 @@ void main() {
expect(editorState.selection!.start.offset, 0);
});
testWidgets('select and delete text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// create a new document
await tester.createNewPageWithNameUnderParent();
/// input text
final editor = tester.editor;
final editorState = editor.getCurrentEditorState();
const inputText = 'Test for text selection and deletion';
final texts = inputText.split(' ');
await editor.tapLineOfEditorAt(0);
await tester.ime.insertText(inputText);
/// selecte and delete
int index = 0;
while (texts.isNotEmpty) {
final text = texts.removeAt(0);
await tester.editor.updateSelection(
Selection(
start: Position(path: [0], offset: index),
end: Position(path: [0], offset: index + text.length),
),
);
await tester.simulateKeyEvent(LogicalKeyboardKey.delete);
index++;
}
/// excpete the text value is correct
final node = editorState.getNodeAtPath([0])!;
final nodeText = node.delta?.toPlainText() ?? '';
expect(nodeText, ' ' * (index - 1));
});
});
}

View file

@ -58,11 +58,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
findsNWidgets(3),
@ -77,12 +72,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.pumpAndSettle();
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -101,11 +90,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -154,11 +138,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -212,11 +191,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -253,11 +227,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -303,11 +272,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -346,11 +310,6 @@ void main() {
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -394,11 +353,6 @@ void main() {
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
expect(find.byType(SubPageBlockComponent), findsOneWidget);
@ -421,12 +375,6 @@ void main() {
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
await tester.insertSubPageFromSlashMenu();
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
expect(find.text('Child page'), findsNWidgets(2));
@ -447,11 +395,6 @@ void main() {
await tester.insertSubPageFromSlashMenu(true);
await tester.expandOrCollapsePage(
pageName: 'SubPageBlock',
layout: ViewLayoutPB.Document,
);
expect(find.byType(SubPageBlockComponent), findsOneWidget);
final beforeNode = tester.editor.getNodeAtPath([1]);
@ -532,6 +475,11 @@ void main() {
await tester.pumpAndSettle();
await tester.openPage(firstPage);
await tester.expandOrCollapsePage(
pageName: firstPage,
layout: ViewLayoutPB.Document,
);
/// check if there is a icon in document
final iconWidget = find.byWidgetPredicate((w) {
if (w is! RawEmojiIconWidget) return false;

View file

@ -13,6 +13,7 @@ import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_simple_table_test.dart'
as document_with_simple_table_test;
import 'document_link_preview_test.dart' as document_link_preview_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -28,4 +29,5 @@ void main() {
document_find_menu_test.main();
document_toolbar_test.main();
document_with_simple_table_test.main();
document_link_preview_test.main();
}

View file

@ -1,5 +1,19 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -8,24 +22,33 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Future<void> selectText(WidgetTester tester, String text) async {
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
}
Future<void> prepareForToolbar(WidgetTester tester, String text) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText(text);
await selectText(tester, text);
}
group('document toolbar:', () {
testWidgets('font family', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
const text = 'font family';
await tester.ime.insertText(text);
await tester.editor.updateSelection(
Selection.single(
path: [0],
startOffset: 0,
endOffset: text.length,
),
);
await prepareForToolbar(tester, 'font family');
// tap more options button
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m);
// tap the font family button
final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey);
await tester.tapButton(fontFamilyButton);
@ -46,5 +69,302 @@ void main() {
abel,
);
});
testWidgets('heading 1~3', (tester) async {
const text = 'heading';
await prepareForToolbar(tester, text);
Future<void> testChangeHeading(
FlowySvgData svg,
String title,
int level,
) async {
/// tap suggestions item
final suggestionsButton = find.byKey(kSuggestionsItemKey);
await tester.tapButton(suggestionsButton);
/// tap item
await tester.ensureVisible(find.byFlowySvg(svg));
await tester.tapButton(find.byFlowySvg(svg));
/// check the type of node is [HeadingBlockKeys.type]
await selectText(tester, text);
final editorState = tester.editor.getCurrentEditorState();
final selection = editorState.selection!;
final node = editorState.getNodeAtPath(selection.start.path)!,
nodeLevel = node.attributes[HeadingBlockKeys.level]!;
expect(node.type, HeadingBlockKeys.type);
expect(nodeLevel, level);
/// show toolbar again
await selectText(tester, text);
/// the text of suggestions item should be changed
expect(
find.descendant(of: suggestionsButton, matching: find.text(title)),
findsOneWidget,
);
}
await testChangeHeading(
FlowySvgs.type_h1_m,
LocaleKeys.document_toolbar_h1.tr(),
1,
);
await testChangeHeading(
FlowySvgs.type_h2_m,
LocaleKeys.document_toolbar_h2.tr(),
2,
);
await testChangeHeading(
FlowySvgs.type_h3_m,
LocaleKeys.document_toolbar_h3.tr(),
3,
);
});
testWidgets('toggle 1~3', (tester) async {
const text = 'toggle';
await prepareForToolbar(tester, text);
Future<void> testChangeToggle(
FlowySvgData svg,
String title,
int? level,
) async {
/// tap suggestions item
final suggestionsButton = find.byKey(kSuggestionsItemKey);
await tester.tapButton(suggestionsButton);
/// tap item
await tester.ensureVisible(find.byFlowySvg(svg));
await tester.tapButton(find.byFlowySvg(svg));
/// check the type of node is [HeadingBlockKeys.type]
await selectText(tester, text);
final editorState = tester.editor.getCurrentEditorState();
final selection = editorState.selection!;
final node = editorState.getNodeAtPath(selection.start.path)!,
nodeLevel = node.attributes[ToggleListBlockKeys.level];
expect(node.type, ToggleListBlockKeys.type);
expect(nodeLevel, level);
/// show toolbar again
await selectText(tester, text);
/// the text of suggestions item should be changed
expect(
find.descendant(of: suggestionsButton, matching: find.text(title)),
findsOneWidget,
);
}
await testChangeToggle(
FlowySvgs.type_toggle_list_m,
LocaleKeys.editor_toggleListShortForm.tr(),
null,
);
await testChangeToggle(
FlowySvgs.type_toggle_h1_m,
LocaleKeys.editor_toggleHeading1ShortForm.tr(),
1,
);
await testChangeToggle(
FlowySvgs.type_toggle_h2_m,
LocaleKeys.editor_toggleHeading2ShortForm.tr(),
2,
);
await testChangeToggle(
FlowySvgs.type_toggle_h3_m,
LocaleKeys.editor_toggleHeading3ShortForm.tr(),
3,
);
});
testWidgets('toolbar will not rebuild after click item', (tester) async {
const text = 'Test rebuilding';
await prepareForToolbar(tester, text);
Finder toolbar = find.byType(DesktopFloatingToolbar);
Element toolbarElement = toolbar.evaluate().first;
final elementHashcode = toolbarElement.hashCode;
final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m),
underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m),
italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m);
/// tap format buttons
await tester.tapButton(boldButton);
await tester.tapButton(underlineButton);
await tester.tapButton(italicButton);
toolbar = find.byType(DesktopFloatingToolbar);
toolbarElement = toolbar.evaluate().first;
/// check if the toolbar is not rebuilt
expect(elementHashcode, toolbarElement.hashCode);
final editorState = tester.editor.getCurrentEditorState();
/// check text formats
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold),
true,
);
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic),
true,
);
expect(
editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline),
true,
);
});
});
group('document toolbar: link', () {
String? getLinkFromNode(Node node) {
for (final insert in node.delta!) {
final link = insert.attributes?.href;
if (link != null) return link;
}
return null;
}
bool isPageLink(Node node) {
for (final insert in node.delta!) {
final isPage = insert.attributes?.isPage;
if (isPage == true) return true;
}
return false;
}
String getNodeText(Node node) {
for (final insert in node.delta!) {
if (insert is TextInsert) return insert.text;
}
return '';
}
testWidgets('insert link and remove link', (tester) async {
const text = 'insert link', link = 'https://test.appflowy.cloud';
await prepareForToolbar(tester, text);
final toolbar = find.byType(DesktopFloatingToolbar);
expect(toolbar, findsOneWidget);
/// tap link button to show CreateLinkMenu
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
await tester.tapButton(linkButton);
final createLinkMenu = find.byType(LinkCreateMenu);
expect(createLinkMenu, findsOneWidget);
/// test esc to close
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
expect(toolbar, findsNothing);
/// show toolbar again
await tester.editor.tapLineOfEditorAt(0);
await selectText(tester, text);
await tester.tapButton(linkButton);
/// insert link
final textField = find.descendant(
of: createLinkMenu,
matching: find.byType(TextFormField),
);
await tester.enterText(textField, link);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
Node node = tester.editor.getNodeAtPath([0]);
expect(getLinkFromNode(node), link);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
/// hover link
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
final hoverMenu = find.byType(LinkHoverMenu);
expect(hoverMenu, findsOneWidget);
/// copy link
final copyButton = find.descendant(
of: hoverMenu,
matching: find.byFlowySvg(FlowySvgs.toolbar_link_m),
);
await tester.tapButton(copyButton);
final clipboardContent = await getIt<ClipboardService>().getData();
final plainText = clipboardContent.plainText;
expect(plainText, link);
/// remove link
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
node = tester.editor.getNodeAtPath([0]);
expect(getLinkFromNode(node), null);
});
testWidgets('insert link and edit link', (tester) async {
const text = 'edit link',
link = 'https://test.appflowy.cloud',
afterText = '$text after';
await prepareForToolbar(tester, text);
/// tap link button to show CreateLinkMenu
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
await tester.tapButton(linkButton);
/// search for page and select it
final textField = find.descendant(
of: find.byType(LinkCreateMenu),
matching: find.byType(TextFormField),
);
await tester.enterText(textField, gettingStarted);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
Node node = tester.editor.getNodeAtPath([0]);
expect(isPageLink(node), true);
expect(getLinkFromNode(node) == link, false);
/// hover link
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
/// click edit button to show LinkEditMenu
final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);
await tester.tapButton(editButton);
final linkEditMenu = find.byType(LinkEditMenu);
expect(linkEditMenu, findsOneWidget);
/// change the link text
final titleField = find.descendant(
of: linkEditMenu,
matching: find.byType(TextFormField),
);
await tester.enterText(titleField, afterText);
await tester.pumpAndSettle();
await tester.tapButton(
find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)),
);
final linkField = find.ancestor(
of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()),
matching: find.byType(TextFormField),
);
await tester.enterText(linkField, link);
await tester.pumpAndSettle();
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
/// apply the change
final applyButton =
find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr());
await tester.tapButton(applyButton);
node = tester.editor.getNodeAtPath([0]);
expect(isPageLink(node), false);
expect(getLinkFromNode(node), link);
expect(getNodeText(node), afterText);
});
});
}

View file

@ -1,15 +1,23 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
@ -60,6 +68,59 @@ void main() {
tester.expectToSeeNoDocumentCover();
});
testWidgets('document cover local image tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
// Insert a document cover
await tester.editor.tapOnAddCover();
tester.expectToSeeDocumentCover(CoverType.asset);
// Hover over the cover to show the 'Change Cover' and delete buttons
await tester.editor.hoverOnCover();
tester.expectChangeCoverAndDeleteButton();
// Change cover to a local image image
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
await tester.editor.hoverOnCover();
await tester.editor.tapOnChangeCover();
final uploadButton = find.findTextInFlowyText(
LocaleKeys.document_imageBlock_upload_label.tr(),
);
await tester.tapButton(uploadButton);
mockPickFilePaths(paths: [localImagePath]);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
tester.expectToSeeDocumentCover(CoverType.file);
// Remove the cover
await tester.editor.hoverOnCover();
await tester.editor.tapOnRemoveCover();
tester.expectToSeeNoDocumentCover();
// Test if deleteImageFromLocalStorage(localImagePath) function is called once
await tester.pump(kDoubleTapTimeout);
expect(deleteImageTestCounter, 1);
// delete temp files
await imageFile.delete();
});
testWidgets('document icon tests', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();

View file

@ -7,19 +7,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:run_with_network_images/run_with_network_images.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
@ -29,58 +26,6 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('image block in document', () {
Future<void> testEmbedImage(WidgetTester tester, String url) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], url);
}
testWidgets('insert an image from local file', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@ -131,43 +76,6 @@ void main() {
file.deleteSync();
});
testWidgets('insert a gif image from network', (tester) async {
await testEmbedImage(
tester,
'https://www.easygifanimator.net/images/samples/sparkles.gif',
);
});
testWidgets('insert an image from unsplash', (tester) async {
await runWithNetworkImages(() async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
expect(find.text('Unsplash'), findsOneWidget);
});
});
testWidgets('insert two images from local file at once', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();

View file

@ -1,9 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -32,9 +34,15 @@ void main() {
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the more options button
final moreOptionButton = find.findFlowyTooltip(
LocaleKeys.document_toolbar_moreOptions.tr(),
);
await tester.tapButton(moreOptionButton);
// tap the inline math equation button
final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
final inlineMathEquationButton = find.text(
LocaleKeys.document_toolbar_equation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
@ -77,10 +85,15 @@ void main() {
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the inline math equation button
var inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
// tap the more options button
final moreOptionButton = find.findFlowyTooltip(
LocaleKeys.document_toolbar_moreOptions.tr(),
);
await tester.tapButton(moreOptionButton);
// tap the inline math equation button
final inlineMathEquationButton =
find.byFlowySvg(FlowySvgs.type_formula_m);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
@ -92,17 +105,7 @@ void main() {
Selection.single(path: [0], startOffset: 0, endOffset: 1),
);
// expect to the see the inline math equation button is highlighted
inlineMathEquationButton = find.descendant(
of: find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
),
matching: find.byType(SVGIconItemWidget),
);
expect(
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,
isTrue,
);
await tester.tapButton(moreOptionButton);
// cancel the format
await tester.tapButton(inlineMathEquationButton);
@ -133,10 +136,15 @@ void main() {
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// tap the inline math equation button
final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
// tap the more options button
final moreOptionButton = find.findFlowyTooltip(
LocaleKeys.document_toolbar_moreOptions.tr(),
);
await tester.tapButton(moreOptionButton);
// tap the inline math equation button
final inlineMathEquationButton =
find.byFlowySvg(FlowySvgs.type_formula_m);
await tester.tapButton(inlineMathEquationButton);
// expect to see the math equation block
@ -163,5 +171,55 @@ void main() {
lessThan(5),
);
});
testWidgets('insert inline math equation by shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'insert inline math equation by shortcut',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const formula = 'E = MC ^ 2';
await tester.ime.insertText(formula);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// mock key event
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyE,
isShiftPressed: true,
isControlPressed: true,
);
// expect to see the math equation block
final inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
await tester.editor.tapLineOfEditorAt(0);
const text = 'Hello World';
await tester.ime.insertText(text);
final inlineText = find.textContaining(text, findRichText: true);
expect(inlineText, findsOneWidget);
// the text should be in the same line with the math equation
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
final textPosition = tester.getRect(inlineText);
// allow 5px difference
expect(
(textPosition.top - inlineMathEquationPosition.top).abs(),
lessThan(5),
);
expect(
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
lessThan(5),
);
});
});
}

View file

@ -48,7 +48,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 80,
offset: 100,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@ -146,7 +146,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 80,
offset: 100,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);

View file

@ -1,3 +1,4 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -85,16 +86,10 @@ void main() {
),
);
await tester.tapButton(find.byType(HeadingPopup));
await tester.pumpAndSettle();
expect(
find.byType(HeadingButton),
findsNWidgets(3),
);
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m));
// tap the H1 button
await tester.tapButton(find.byType(HeadingButton).at(0));
await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0));
await tester.pumpAndSettle();
final editorState = tester.editor.getCurrentEditorState();

View file

@ -35,10 +35,12 @@ void main() {
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(backspaceCmd),
),
find
.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(backspaceCmd),
)
.first,
onHover: () async {
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
await tester.pumpAndSettle();

View file

@ -2,10 +2,10 @@ import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:toastification/toastification.dart';
import '../../shared/util.dart';
@ -23,7 +23,7 @@ void main() {
.last;
}
group('sign-in page settings: ', () {
group('sign-in page settings:', () {
testWidgets('change server type', (tester) async {
await tester.initializeAppFlowy();
@ -45,24 +45,31 @@ void main() {
// change the server type to self-host
await tester.tapButton(appflowyCloudType);
final selfhostedButton = findServerType(
final selfHostedButton = findServerType(
AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapButton(selfhostedButton);
await tester.tapButton(selfHostedButton);
// update server url
const serverUrl = 'https://test.appflowy.cloud';
const serverUrl = 'https://self-hosted.appflowy.cloud';
await tester.enterText(
find.byKey(kSelfHostedTextInputFieldKey),
serverUrl,
);
await tester.pumpAndSettle();
// update the web url
const webUrl = 'https://self-hosted.appflowy.com';
await tester.enterText(
find.byKey(kSelfHostedWebTextInputFieldKey),
webUrl,
);
await tester.pumpAndSettle();
await tester.tapButton(
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpUntilNotFound(find.byType(DesktopToast));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// open settings page to check the result
@ -79,6 +86,11 @@ void main() {
find.text(serverUrl),
findsOneWidget,
);
// check the web url
expect(
find.text(webUrl),
findsOneWidget,
);
// reset to appflowy cloud
await tester.tapButton(
@ -90,7 +102,7 @@ void main() {
);
// wait the app to restart, and the tooltip to disappear
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
await tester.pumpUntilNotFound(find.byType(DesktopToast));
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type

View file

@ -1,8 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -44,5 +48,82 @@ void main() {
);
expect(isExpanded(type: FolderSpaceType.private), true);
});
testWidgets('Expanding with subpage', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
const page1 = 'SubPageBloc', page2 = '$page1 2';
await tester.createNewPageWithNameUnderParent(name: page1);
await tester.createNewPageWithNameUnderParent(
name: page2,
parentName: page1,
);
await tester.expandOrCollapsePage(
pageName: gettingStarted,
layout: ViewLayoutPB.Document,
);
await tester.tapNewPageButton();
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
final slashMenu = find
.ancestor(
of: find.byType(SelectionMenuItemWidget),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable,
),
)
.first;
final slashMenuItem = find.text(
LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
);
await tester.scrollUntilVisible(
slashMenuItem,
100,
scrollable: slashMenu,
duration: const Duration(milliseconds: 250),
);
final menuItemFinder = find.byWidgetPredicate(
(w) =>
w is SelectionMenuItemWidget &&
w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
);
final menuItem =
menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget;
/// tapSlashMenuItemWithName is not working, so invoke this function directly
menuItem.item.handler(
menuItem.editorState,
menuItem.menuService,
menuItemFinder.evaluate().first,
);
await tester.pumpAndSettle();
final actionHandler = find.byType(InlineActionsHandler);
final subPage = find.descendant(
of: actionHandler,
matching: find.text(page2, findRichText: true),
);
await tester.tapButton(subPage);
final subpageBlock = find.descendant(
of: find.byType(AppFlowyEditor),
matching: find.text(page2, findRichText: true),
);
expect(find.text(page2, findRichText: true), findsOneWidget);
await tester.tapButton(subpageBlock);
/// one is in SectionFolder, another one is in CoverTitle
/// the last one is in FlowyNavigation
expect(find.text(page2, findRichText: true), findsNWidgets(3));
});
});
}

View file

@ -196,5 +196,58 @@ void main() {
await tester.pumpAndSettle();
},
);
testWidgets(
'reorder favorites',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// there are no favorite views
final favorites = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.byType(ViewItem),
);
expect(favorites, findsNothing);
/// create views and then favorite them
const pageNames = ['001', '002', '003'];
for (final name in pageNames) {
await tester.createNewPageWithNameUnderParent(name: name);
}
for (final name in pageNames) {
await tester.favoriteViewByName(name);
}
expect(favorites, findsNWidgets(pageNames.length));
final oldNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(oldNames, pageNames);
/// drag first to last
await tester.reorderFavorite(
fromName: '001',
toName: '003',
);
List<String> newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['002', '003', '001']);
/// drag first to second
await tester.reorderFavorite(
fromName: '002',
toName: '003',
);
newNames = favorites
.evaluate()
.map((e) => (e.widget as ViewItem).view.name)
.toList();
expect(newNames, ['003', '002', '001']);
},
);
});
}

View file

@ -5,6 +5,7 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -209,4 +210,137 @@ void main() {
);
}
});
testWidgets('Update page custom image icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
final iconData = await tester.prepareImageIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
tester.expectViewTitleHasIcon(
value.name,
value,
iconData,
);
}
});
testWidgets('Update page custom svg icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
final iconData = await tester.prepareSvgIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
tester.expectViewTitleHasIcon(
value.name,
value,
iconData,
);
}
});
testWidgets('Update page custom svg icon in title bar by pasting a link',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
const testIconLink =
'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg';
/// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
/// update its icon
await tester.updatePageIconInTitleBarByPasteALink(
name: value.name,
layout: value,
iconLink: testIconLink,
);
/// check if there is a svg in page
final pageName = tester.findPageName(
value.name,
layout: value,
);
final imageInPage = find.descendant(
of: pageName,
matching: find.byType(SvgPicture),
);
expect(imageInPage, findsOneWidget);
/// check if there is a svg in title
final imageInTitle = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byWidgetPredicate((w) {
if (w is! SvgPicture) return false;
final loader = w.bytesLoader;
if (loader is! SvgFileLoader) return false;
return loader.file.path.endsWith('.svg');
}),
);
expect(imageInTitle, findsOneWidget);
}
});
}

View file

@ -0,0 +1,56 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/expectation.dart';
void main() {
testWidgets('Skip the empty group name icon in recent icons', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// clear local data
RecentIcons.clear();
await loadIconGroups();
final groups = kIconGroups!;
final List<RecentIcon> localIcons = [];
for (final e in groups) {
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
}
await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));
await tester.openPage(gettingStarted);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(gettingStarted),
);
await tester.tapButton(title);
/// tap emoji picker button
await tester.tapButton(find.byType(EmojiPickerButton));
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
/// tap icon tab
final pickTab = find.byType(PickerTab);
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.icon.tr),
);
await tester.tapButton(iconTab);
expect(find.byType(FlowyIconPicker), findsOneWidget);
/// no recent icons
final recentText = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.text('Recent'),
);
expect(recentText, findsNothing);
});
}

View file

@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;
import 'sidebar_test.dart' as sidebar_test;
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
@ -14,4 +15,5 @@ void main() {
sidebar_favorite_test.main();
sidebar_icon_test.main();
sidebar_view_item_test.main();
sidebar_recent_icon_test.main();
}

View file

@ -1,42 +1,166 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/emoji/emoji_handler.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Future<void> prepare(WidgetTester tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent();
await tester.editor.tapLineOfEditorAt(0);
}
// May be better to move this to an existing test but unsure what it fits with
group('Keyboard shortcuts related to emojis', () {
testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await prepare(tester);
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
expect(find.byType(EmojiHandler), findsNothing);
expect(find.byType(EmojiSelectionMenu), findsNothing);
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.alt,
LogicalKeyboardKey.keyE,
],
tester: tester,
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyE,
isAltPressed: true,
isMetaPressed: Platform.isMacOS,
isControlPressed: !Platform.isMacOS,
);
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.byType(EmojiHandler), findsOneWidget);
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
/// press backspace to hide the emoji picker
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
expect(find.byType(EmojiHandler), findsNothing);
});
testWidgets('insert emoji by slash menu', (tester) async {
await prepare(tester);
await tester.editor.showSlashMenu();
/// show emoji picler
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_emoji.tr(),
offset: 100,
);
await tester.pumpAndSettle(Duration(seconds: 1));
expect(find.byType(EmojiHandler), findsOneWidget);
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
/// except the emoji is in document
expect(firstNode.delta!.toPlainText().contains('😀'), true);
});
});
group('insert emoji by colon', () {
Future<void> createNewDocumentAndShowEmojiList(
WidgetTester tester, {
String? search,
}) async {
await prepare(tester);
await tester.ime.insertText(':${search ?? 'a'}');
await tester.pumpAndSettle(Duration(seconds: 1));
}
testWidgets('insert with click', (tester) async {
await createNewDocumentAndShowEmojiList(tester);
/// emoji list is showing
final emojiHandler = find.byType(EmojiHandler);
expect(emojiHandler, findsOneWidget);
final emojiButtons =
find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));
final firstTextFinder = find.descendant(
of: emojiButtons.first,
matching: find.byType(FlowyText),
);
final emojiText =
(firstTextFinder.evaluate().first.widget as FlowyText).text;
/// click first emoji item
await tester.tapButton(emojiButtons.first);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
/// except the emoji is in document
expect(emojiText.contains(firstNode.delta!.toPlainText()), true);
});
testWidgets('insert with arrow and enter', (tester) async {
await createNewDocumentAndShowEmojiList(tester);
/// emoji list is showing
final emojiHandler = find.byType(EmojiHandler);
expect(emojiHandler, findsOneWidget);
final emojiButtons =
find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));
/// tap arrow down and arrow up
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);
final firstTextFinder = find.descendant(
of: emojiButtons.first,
matching: find.byType(FlowyText),
);
final emojiText =
(firstTextFinder.evaluate().first.widget as FlowyText).text;
/// tap enter
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
/// except the emoji is in document
expect(emojiText.contains(firstNode.delta!.toPlainText()), true);
});
testWidgets('insert with searching', (tester) async {
await createNewDocumentAndShowEmojiList(tester, search: 's');
/// search for `smiling eyes`, IME is not working, use keyboard input
final searchText = [
LogicalKeyboardKey.keyM,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.keyL,
LogicalKeyboardKey.keyI,
LogicalKeyboardKey.keyN,
LogicalKeyboardKey.keyG,
LogicalKeyboardKey.space,
LogicalKeyboardKey.keyE,
LogicalKeyboardKey.keyY,
LogicalKeyboardKey.keyE,
LogicalKeyboardKey.keyS,
];
for (final key in searchText) {
await tester.simulateKeyEvent(key);
}
/// tap enter
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
/// except the emoji is in document
expect(firstNode.delta!.toPlainText().contains('😄'), true);
});
testWidgets('start searching with sapce', (tester) async {
await createNewDocumentAndShowEmojiList(tester, search: ' ');
/// emoji list is showing
final emojiHandler = find.byType(EmojiHandler);
expect(emojiHandler, findsNothing);
});
});
}

View file

@ -1,13 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -38,7 +37,7 @@ void main() {
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
),
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(milliseconds: 250));
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
@ -48,7 +47,7 @@ void main() {
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
),
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(milliseconds: 250));
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.dark);
@ -66,10 +65,11 @@ void main() {
],
tester: tester,
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(milliseconds: 500));
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
// disable it temporarily. It works on macOS but not on Linux.
// themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
// expect(themeMode, ThemeMode.light);
});
testWidgets('show or hide home menu', (tester) async {

View file

@ -1,12 +1,16 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:archive/archive.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../document/document_with_database_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -18,7 +22,7 @@ void main() {
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.md'),
p.join(context.applicationDataDirectory, 'test.zip'),
);
// click the share button and select markdown
await tester.tapShareButton();
@ -28,10 +32,14 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
final markdown = file.readAsStringSync();
expect(markdown, expectedMarkdown);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.md')) {
final markdown = utf8.decode(entry.content);
expect(markdown, expectedMarkdown);
}
}
});
testWidgets(
@ -57,7 +65,7 @@ void main() {
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.view.name}.md',
'${shareButtonState.view.name}.zip',
),
);
@ -69,10 +77,44 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.md')) {
final markdown = utf8.decode(entry.content);
expect(markdown, expectedMarkdown);
}
}
},
);
testWidgets('share the markdown with database', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.zip'),
);
// click the share button and select markdown
await tester.tapShareButton();
await tester.tapMarkdownButton();
// expect to see the success dialog
tester.expectToExportSuccess();
final file = File(path);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
bool hasCsvFile = false;
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.csv')) {
hasCsvFile = true;
}
}
expect(hasCsvFile, true);
});
});
}

View file

@ -1,11 +1,15 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -285,6 +289,39 @@ void main() {
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
});
testWidgets('displaying icons in tab', (tester) async {
RecentIcons.enable = false;
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
final icon = await tester.loadIcon();
// update emoji
await tester.updatePageIconInSidebarByName(
name: gettingStarted,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
icon: icon,
);
/// create new page
await tester.createNewPageWithNameUnderParent(name: _documentName);
/// open new tab for [gettingStarted]
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
final tabs = find.descendant(
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
);
expect(tabs, findsNWidgets(2));
final svgInTab =
find.descendant(of: tabs.last, matching: find.byType(FlowySvg));
final svgWidget = svgInTab.evaluate().first.widget as FlowySvg;
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
expect(svgWidget.svgString, iconsData.svgString);
});
});
}

View file

@ -13,7 +13,6 @@ void main() {
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
share_markdown_test.main();
import_files_test.main();
zoom_in_out_test.main();

View file

@ -1,9 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/database/database_icon_test.dart' as database_icon_test;
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/uncategorized/code_block_language_selector_test.dart'
as code_language_selector;
import 'desktop/first_test/first_test.dart' as first_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
Future<void> main() async {
await runIntegration9OnDesktop();
@ -15,4 +16,5 @@ Future<void> runIntegration9OnDesktop() async {
first_test.main();
tabs_test.main();
code_language_selector.main();
database_icon_test.main();
}

View file

@ -0,0 +1,56 @@
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const title = 'Test At Menu';
group('at menu', () {
testWidgets('show at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
final menuWidget = find.byType(MobileInlineActionsMenu);
expect(menuWidget, findsOneWidget);
});
testWidgets('search by at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
expect(actionWidgets, findsNWidgets(2));
});
testWidgets('tap at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tap(actionWidgets.last);
expect(find.byType(MentionPageBlock), findsOneWidget);
});
testWidgets('create subpage with at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile(title);
await tester.editor.tapLineOfEditorAt(0);
const subpageName = 'Subpage';
await tester.ime.insertText('[[$subpageName');
await tester.pumpAndSettle();
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tapButton(actionWidgets.first);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0]);
assert(firstNode != null);
expect(firstNode!.delta?.toPlainText().contains('['), false);
});
});
}

View file

@ -1,9 +1,13 @@
import 'package:integration_test/integration_test.dart';
import 'at_menu_test.dart' as at_menu;
import 'at_menu_test.dart' as at_menu_test;
import 'page_style_test.dart' as page_style_test;
import 'plus_menu_test.dart' as plus_menu_test;
import 'simple_table_test.dart' as simple_table_test;
import 'slash_menu_test.dart' as slash_menu;
import 'title_test.dart' as title_test;
import 'toolbar_test.dart' as toolbar_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -12,5 +16,9 @@ void main() {
title_test.main();
page_style_test.main();
plus_menu_test.main();
at_menu_test.main();
simple_table_test.main();
toolbar_test.main();
slash_menu.main();
at_menu.main();
}

View file

@ -0,0 +1,104 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('update page custom image icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local image
final iconData = await tester.prepareImageIcon();
/// create an empty page
await tester
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
/// show Page style page
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
final pageStyleIcon = find.byType(PageStyleIcon);
final iconInPageStyleIcon = find.descendant(
of: pageStyleIcon,
matching: find.byType(RawEmojiIconWidget),
);
expect(iconInPageStyleIcon, findsNothing);
/// show icon picker
await tester.tapButton(pageStyleIcon);
/// upload custom icon
await tester.pickImage(iconData);
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconFinder = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.last;
final rawEmojiIconWidget =
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
final iconDataInWidget = rawEmojiIconWidget.emoji;
expect(iconDataInWidget.type, FlowyIconType.custom);
final imageFinder =
find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image));
expect(imageFinder, findsOneWidget);
});
testWidgets('update page custom svg icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local image
final iconData = await tester.prepareSvgIcon();
/// create an empty page
await tester
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
/// show Page style page
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
final pageStyleIcon = find.byType(PageStyleIcon);
final iconInPageStyleIcon = find.descendant(
of: pageStyleIcon,
matching: find.byType(RawEmojiIconWidget),
);
expect(iconInPageStyleIcon, findsNothing);
/// show icon picker
await tester.tapButton(pageStyleIcon);
/// upload custom icon
await tester.pickImage(iconData);
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconFinder = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.last;
final rawEmojiIconWidget =
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
final iconDataInWidget = rawEmojiIconWidget.emoji;
expect(iconDataInWidget.type, FlowyIconType.custom);
final svgFinder = find.descendant(
of: rawEmojiIconFinder,
matching: find.byType(SvgPicture),
);
expect(svgFinder, findsOneWidget);
});
});
}

View file

@ -1,6 +1,9 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@ -85,5 +88,32 @@ void main() {
equals(Selection.collapsed(Position(path: [2]))),
);
});
const title = 'Test Plus Menu';
testWidgets('show plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
final menuWidget = find.byType(MobileInlineActionsMenu);
expect(menuWidget, findsOneWidget);
});
testWidgets('search by plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
expect(actionWidgets, findsNWidgets(2));
});
testWidgets('tap plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tap(actionWidgets.last);
expect(find.byType(MentionPageBlock), findsOneWidget);
});
});
}

View file

@ -494,5 +494,61 @@ void main() {
expect(heading.level, equals(1));
}
});
testWidgets('''
1. insert a simple table via + menu
2. resize column
''', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile('simple table');
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: [0])),
reason: SelectionUpdateReason.uiEvent,
),
);
await tester.pumpAndSettle();
final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
// find the first cell
{
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
final offset = tester.getCenter(resizeHandle);
final gesture = await tester.startGesture(offset, pointer: 7);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(100, 0));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
}
// check the table is updated
final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
expect(afterWidth1, greaterThan(beforeWidth));
// resize back to the original width
{
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
final offset = tester.getCenter(resizeHandle);
final gesture = await tester.startGesture(offset, pointer: 7);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(-100, 0));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
}
// check the table is updated
final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
expect(afterWidth2, equals(beforeWidth));
});
});
}

View file

@ -0,0 +1,84 @@
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const title = 'Test Slash Menu';
group('slash menu', () {
testWidgets('show slash menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowSlashMenu(title);
final menuWidget = find.byType(MobileSelectionMenuWidget);
expect(menuWidget, findsOneWidget);
final items =
(menuWidget.evaluate().first.widget as MobileSelectionMenuWidget)
.items;
int i = 0;
for (final item in items) {
final localItem = mobileItems[i];
expect(item.name, localItem.name);
i++;
}
});
testWidgets('search by slash menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowSlashMenu(title);
const searchText = 'Heading';
await tester.ime.insertText(searchText);
final itemWidgets = find.byType(MobileSelectionMenuItemWidget);
int number = 0;
for (final item in mobileItems) {
if (item is MobileSelectionMenuItem) {
for (final childItem in item.children) {
if (childItem.name
.toLowerCase()
.contains(searchText.toLowerCase())) {
number++;
}
}
} else {
if (item.name.toLowerCase().contains(searchText.toLowerCase())) {
number++;
}
}
}
expect(itemWidgets, findsNWidgets(number));
});
testWidgets('tap to show submenu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile(title);
await tester.editor.tapLineOfEditorAt(0);
final listview = find.descendant(
of: find.byType(MobileSelectionMenuWidget),
matching: find.byType(ListView),
);
for (final item in mobileItems) {
if (item is! MobileSelectionMenuItem) continue;
await tester.editor.showSlashMenu();
await tester.scrollUntilVisible(
find.text(item.name),
50,
scrollable: listview,
duration: const Duration(milliseconds: 250),
);
await tester.tap(find.text(item.name));
final childrenLength = ((listview.evaluate().first.widget as ListView)
.childrenDelegate as SliverChildListDelegate)
.children
.length;
expect(childrenLength, item.children.length);
}
});
});
}

View file

@ -0,0 +1,117 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('toolbar menu:', () {
testWidgets('insert links', (tester) async {
await tester.launchInAnonymousMode();
final createPageButton = find.byKey(
BottomNavigationBarItemType.add.valueKey,
);
await tester.tapButton(createPageButton);
expect(find.byType(MobileDocumentScreen), findsOneWidget);
final editor = find.byType(AppFlowyEditor);
expect(editor, findsOneWidget);
final editorState = tester.editor.getCurrentEditorState();
/// move cursor to content
final root = editorState.document.root;
final lastNode = root.children.lastOrNull;
await editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: lastNode!.path)),
);
await tester.pumpAndSettle();
/// insert two lines of text
const strFirst = 'FirstLine',
strSecond = 'SecondLine',
link = 'google.com';
await editorState.insertTextAtCurrentSelection(strFirst);
await tester.pumpAndSettle();
await editorState.insertNewLine();
await tester.pumpAndSettle();
await editorState.insertTextAtCurrentSelection(strSecond);
await tester.pumpAndSettle();
final firstLine = find.text(strFirst, findRichText: true),
secondLine = find.text(strSecond, findRichText: true);
expect(firstLine, findsOneWidget);
expect(secondLine, findsOneWidget);
/// select the first line
await tester.longPress(firstLine);
await tester.pumpAndSettle();
/// find aa item and tap it
final aaItem = find.byWidgetPredicate(
(widget) =>
widget is AppFlowyMobileToolbarIconItem &&
widget.icon == FlowySvgs.m_toolbar_aa_m,
);
expect(aaItem, findsOneWidget);
await tester.tapButton(aaItem);
/// find link button and tap it
final linkButton = find.byWidgetPredicate(
(widget) =>
widget is MobileToolbarMenuItemWrapper &&
widget.icon == FlowySvgs.m_toolbar_link_m,
);
expect(linkButton, findsOneWidget);
await tester.tapButton(linkButton);
/// input the link
final linkField = find.byWidgetPredicate(
(w) =>
w is FlowyTextField &&
w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(),
);
await tester.enterText(linkField, link);
await tester.pumpAndSettle();
/// complete inputting
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
/// do it again
/// select the second line
await tester.longPress(secondLine);
await tester.pumpAndSettle();
await tester.tapButton(aaItem);
await tester.tapButton(linkButton);
await tester.enterText(linkField, link);
await tester.pumpAndSettle();
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
final firstNode = editorState.getNodeAtPath([0]);
final secondNode = editorState.getNodeAtPath([1]);
Map commonDeltaJson(String insert) => {
"insert": insert,
"attributes": {"href": link},
};
expect(
firstNode?.delta?.toJson(),
commonDeltaJson(strFirst),
);
expect(
secondNode?.delta?.toJson(),
commonDeltaJson(strSecond),
);
});
});
}

View file

@ -0,0 +1,81 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Change default text direction', (tester) async {
await tester.launchInAnonymousMode();
/// tap [Setting] button
await tester.tapButton(find.byType(HomePageSettingsPopupMenu));
await tester
.tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));
/// tap [Default Text Direction]
await tester.tapButton(
find.text(LocaleKeys.settings_appearance_textDirection_label.tr()),
);
/// there are 3 items: LTR-RTL-AUTO
final bottomSheet = find.ancestor(
of: find.byType(FlowyOptionTile),
matching: find.byType(SafeArea),
);
final items = find.descendant(
of: bottomSheet,
matching: find.byType(FlowyOptionTile),
);
expect(items, findsNWidgets(3));
/// select [Auto]
await tester.tapButton(items.last);
expect(
find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()),
findsOneWidget,
);
/// go back home
await tester.tapButton(find.byType(AppBarImmersiveBackButton));
/// create new page
final createPageButton =
find.byKey(BottomNavigationBarItemType.add.valueKey);
await tester.tapButton(createPageButton);
final editorState = tester.editor.getCurrentEditorState();
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
const testEnglish = 'English', testArabic = 'إنجليزي';
/// insert [testEnglish]
await editorState.insertTextAtCurrentSelection(testEnglish);
await tester.pumpAndSettle();
await editorState.insertNewLine(position: editorState.selection!.end);
await tester.pumpAndSettle();
/// insert [testArabic]
await editorState.insertTextAtCurrentSelection(testArabic);
await tester.pumpAndSettle();
final testEnglishFinder = find.text(testEnglish, findRichText: true),
testArabicFinder = find.text(testArabic, findRichText: true);
final testEnglishRenderBox =
testEnglishFinder.evaluate().first.renderObject as RenderBox,
testArabicRenderBox =
testArabicFinder.evaluate().first.renderObject as RenderBox;
final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero),
arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero);
expect(englishPosition.dx > arabicPosition.dx, true);
});
}

View file

@ -0,0 +1,48 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('test for change scale factor', (tester) async {
await tester.launchInAnonymousMode();
/// tap [Setting] button
await tester.tapButton(find.byType(HomePageSettingsPopupMenu));
await tester
.tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));
/// tap [Font Scale Factor]
await tester.tapButton(
find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()),
);
/// drag slider
final slider = find.descendant(
of: find.byType(FontSizeStepper),
matching: find.byType(Slider),
);
await tester.slideToValue(slider, 0.8);
expect(appflowyScaleFactor, 0.8);
await tester.slideToValue(slider, 0.9);
expect(appflowyScaleFactor, 0.9);
await tester.slideToValue(slider, 1.0);
expect(appflowyScaleFactor, 1.0);
await tester.slideToValue(slider, 1.1);
expect(appflowyScaleFactor, 1.1);
await tester.slideToValue(slider, 1.2);
expect(appflowyScaleFactor, 1.2);
});
}

View file

@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
import 'mobile/document/document_test_runner.dart' as document_test_runner;
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/settings/default_text_direction_test.dart'
as default_text_direction_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> main() async {
@ -17,4 +19,5 @@ Future<void> runIntegration1OnMobile() async {
anonymous_sign_in_test.main();
create_new_page_test.main();
document_test_runner.main();
default_text_direction_test.main();
}

View file

@ -13,6 +13,7 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
@ -235,6 +236,25 @@ extension AppFlowyTestBase on WidgetTester {
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> slideToValue(
Finder slider,
double value, {
double paddingOffset = 24.0,
}) async {
final sliderWidget = slider.evaluate().first.widget as Slider;
final range = sliderWidget.max - sliderWidget.min;
final initialRate = (value - sliderWidget.min) / range;
final totalWidth = getSize(slider).width - (2 * paddingOffset);
final zeroPoint = getTopLeft(slider) +
Offset(
paddingOffset + initialRate * totalWidth,
getSize(slider).height / 2,
);
final calculatedOffset = value * (totalWidth / 100);
await dragFrom(zeroPoint, Offset(calculatedOffset, 0));
await pumpAndSettle();
}
}
extension AppFlowyFinderTestBase on CommonFinders {

View file

@ -19,6 +19,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
@ -49,6 +50,8 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:universal_platform/universal_platform.dart';
import 'emoji.dart';
@ -64,12 +67,10 @@ extension CommonOperations on WidgetTester {
} else {
// cloud version
final anonymousButton = find.byType(SignInAnonymousButtonV2);
await tapButton(anonymousButton);
await tapButton(anonymousButton, warnIfMissed: true);
}
if (Platform.isWindows) {
await pumpAndSettle(const Duration(milliseconds: 200));
}
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> tapContinousAnotherWay() async {
@ -600,6 +601,23 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> reorderFavorite({
required String fromName,
required String toName,
}) async {
final from = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(fromName),
),
to = find.descendant(
of: find.byType(FavoriteFolder),
matching: find.text(toName),
);
final distanceY = getCenter(to).dy - getCenter(from).dx;
await drag(from, Offset(0, distanceY));
await pumpAndSettle(const Duration(seconds: 1));
}
// tap the button with [FlowySvgData]
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
@ -611,7 +629,7 @@ extension CommonOperations on WidgetTester {
// update the page icon in the sidebar
Future<void> updatePageIconInSidebarByName({
required String name,
required String parentName,
String? parentName,
required ViewLayoutPB layout,
required EmojiIconData icon,
}) async {
@ -653,10 +671,31 @@ extension CommonOperations on WidgetTester {
await tapEmoji(icon.emoji);
} else if (icon.type == FlowyIconType.icon) {
await tapIcon(icon);
} else if (icon.type == FlowyIconType.custom) {
await pickImage(icon);
}
await pumpAndSettle();
}
Future<void> updatePageIconInTitleBarByPasteALink({
required String name,
required ViewLayoutPB layout,
required String iconLink,
}) async {
await openPage(
name,
layout: layout,
);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(name),
);
await tapButton(title);
await tapButton(find.byType(EmojiPickerButton));
await pasteImageLinkAsIcon(iconLink);
await pumpAndSettle();
}
Future<void> openNotificationHub({int tabIndex = 0}) async {
final finder = find.descendant(
of: find.byType(NotificationButton),
@ -910,12 +949,50 @@ extension CommonOperations on WidgetTester {
return EmojiIconData.icon(
IconsData(
firstGroup.name,
firstIcon.content,
firstIcon.name,
builtInSpaceColors.first,
),
);
}
Future<EmojiIconData> prepareImageIcon() async {
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
return EmojiIconData.custom(imageFile.path);
}
Future<EmojiIconData> prepareSvgIcon() async {
final imagePath = await rootBundle.load('assets/test/images/sample.svg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.svg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
return EmojiIconData.custom(imageFile.path);
}
/// create new page and show slash menu
Future<void> createPageAndShowSlashMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showSlashMenu();
}
/// create new page and show at menu
Future<void> createPageAndShowAtMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showAtMenu();
}
/// create new page and show plus menu
Future<void> createPageAndShowPlusMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showPlusMenu();
}
}
extension SettingsFinder on CommonFinders {

View file

@ -1,17 +1,9 @@
import 'dart:io';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/application/field/filter_entities.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
@ -27,10 +19,11 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
@ -44,6 +37,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
@ -76,6 +70,8 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio
import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
@ -90,6 +86,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text_input.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
// Non-exported member of the table_calendar library
@ -943,6 +942,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> changeFieldWidth(String fieldName, double width) async {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
await hoverOnWidget(
field,
onHover: () async {
final dragHandle = find.descendant(
of: field,
matching: find.byType(DragToExpandLine),
);
await drag(dragHandle, Offset(width - getSize(field).width, 0));
await pumpAndSettle(const Duration(milliseconds: 200));
},
);
}
double getFieldWidth(String fieldName) {
final field = find.byWidgetPredicate(
(widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,
);
return getSize(field).width;
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
@ -1464,6 +1488,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await tapButton(button);
await tapButtonWithName(LocaleKeys.button_delete.tr());
}
Future<void> dragDropRescheduleCalendarEvent() async {
@ -1571,7 +1596,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
of: textField,
matching: find.byWidgetPredicate(
(widget) =>
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m,
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s,
),
),
);

View file

@ -307,9 +307,11 @@ class EditorOperations {
Future<void> openTurnIntoMenu(Path path) async {
await hoverAndClickOptionMenuButton(path);
await tester.tapButton(
find.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
),
find
.findTextInFlowyText(
LocaleKeys.document_plugins_optionAction_turnInto.tr(),
)
.first,
);
await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu));
}

View file

@ -1,16 +1,24 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
import 'common_operations.dart';
extension EmojiTestExtension on WidgetTester {
Future<void> tapEmoji(String emoji) async {
@ -21,7 +29,7 @@ extension EmojiTestExtension on WidgetTester {
await tapButton(emojiWidget);
}
Future<void> tapIcon(EmojiIconData icon) async {
Future<void> tapIcon(EmojiIconData icon, {bool enableColor = true}) async {
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
@ -35,33 +43,102 @@ extension EmojiTestExtension on WidgetTester {
final selectedSvg = find.descendant(
of: find.byType(FlowyIconPicker),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(find.byType(IconColorPicker), findsNothing);
/// test for tapping down, it should not display the ColorPicker unless tapping up
await tapDown(selectedSvg);
expect(find.byType(IconColorPicker), findsNothing);
await tapButton(selectedSvg);
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);
final selectedColor = find.descendant(
of: colorPicker,
matching: find.byWidgetPredicate((w) {
if (w is Container) {
final d = w.decoration;
if (d is ShapeDecoration) {
if (d.color ==
Color(int.parse(iconsData.color ?? builtInSpaceColors.first))) {
return true;
await tapButton(selectedSvg.first);
if (enableColor) {
final colorPicker = find.byType(IconColorPicker);
expect(colorPicker, findsOneWidget);
final selectedColor = find.descendant(
of: colorPicker,
matching: find.byWidgetPredicate((w) {
if (w is Container) {
final d = w.decoration;
if (d is ShapeDecoration) {
if (d.color ==
Color(
int.parse(iconsData.color ?? builtInSpaceColors.first),
)) {
return true;
}
}
}
}
return false;
}),
return false;
}),
);
await tapButton(selectedColor);
}
}
Future<void> pickImage(EmojiIconData icon) async {
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
/// switch to custom tab
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.custom.tr),
);
await tapButton(selectedColor);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
/// mock for dragging image
final dropTarget = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(DropTarget),
);
expect(dropTarget, findsOneWidget);
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
dropTargetWidget.onDragDone?.call(
DropDoneDetails(
files: [DropItemFile(icon.emoji)],
localPosition: Offset.zero,
globalPosition: Offset.zero,
),
);
await pumpAndSettle(const Duration(seconds: 3));
/// confirm to upload
final confirmButton = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(PrimaryRoundedButton),
);
await tapButton(confirmButton);
}
Future<void> pasteImageLinkAsIcon(String link) async {
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
/// switch to custom tab
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.custom.tr),
);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
// mock the clipboard
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: link));
// paste the link
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle(const Duration(seconds: 5));
/// confirm to upload
final confirmButton = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(PrimaryRoundedButton),
);
await tapButton(confirmButton);
}
}

View file

@ -7,6 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/appflowy_network_svg.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
@ -21,6 +23,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
import 'util.dart';
@ -245,10 +248,27 @@ extension Expectation on WidgetTester {
final icon = find.descendant(
of: pageName,
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.custom) {
final isSvg = data.emoji.endsWith('.svg');
if (isURL(data.emoji)) {
final image = find.descendant(
of: pageName,
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: pageName,
matching: isSvg ? find.byType(SvgPicture) : find.byType(Image),
);
expect(image, findsOneWidget);
}
}
}
@ -269,10 +289,34 @@ extension Expectation on WidgetTester {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byWidgetPredicate(
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
),
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.custom) {
final isSvg = data.emoji.endsWith('.svg');
if (isURL(data.emoji)) {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: isSvg
? find.byWidgetPredicate((w) {
if (w is! SvgPicture) return false;
final loader = w.bytesLoader;
if (loader is! SvgFileLoader) return false;
return loader.file.path.endsWith('.svg');
})
: find.byType(Image),
);
expect(image, findsOneWidget);
}
}
}

View file

@ -1,81 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
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(AIError 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);
String 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();
}
}
}
}
}

View file

@ -79,7 +79,7 @@ extension AppFlowySettings on WidgetTester {
// Enable editing username
final editUsernameFinder = find.descendant(
of: find.byType(AccountUserProfile),
matching: find.byFlowySvg(FlowySvgs.edit_s),
matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),
);
await tap(editUsernameFinder, warnIfMissed: false);
await pumpAndSettle();

View file

@ -1,5 +1,5 @@
PODS:
- app_links (0.0.1):
- app_links (0.0.2):
- Flutter
- appflowy_backend (0.0.1):
- Flutter
@ -66,6 +66,8 @@ PODS:
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
@ -79,7 +81,7 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
@ -90,6 +92,7 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@ -108,13 +111,14 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
@ -159,52 +163,55 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca

View file

@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View file

@ -1,75 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FLTEnableImpeller</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FLTEnableImpeller</key>
<false />
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your photo library</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>NSCameraUsageDescription</key>
<string>AppFlowy needs access to your camera to let you add images to your documents from
camera</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true />
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>AppFlowy needs access to your camera to let you add images to your documents from camera</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
</plist>

View file

@ -0,0 +1,19 @@
export 'service/ai_entities.dart';
export 'service/ai_prompt_input_bloc.dart';
export 'service/appflowy_ai_service.dart';
export 'service/error.dart';
export 'service/ai_model_state_notifier.dart';
export 'service/select_model_bloc.dart';
export 'widgets/loading_indicator.dart';
export 'widgets/prompt_input/action_buttons.dart';
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
export 'widgets/prompt_input/file_attachment_list.dart';
export 'widgets/prompt_input/layout_define.dart';
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
export 'widgets/prompt_input/mention_page_menu.dart';
export 'widgets/prompt_input/mentioned_page_text_span.dart';
export 'widgets/prompt_input/predefined_format_buttons.dart';
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
export 'widgets/prompt_input/select_sources_menu.dart';
export 'widgets/prompt_input/select_model_menu.dart';
export 'widgets/prompt_input/send_button.dart';

View file

@ -0,0 +1,107 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
class AIStreamEventPrefix {
static const data = 'data:';
static const error = 'error:';
static const metadata = 'metadata:';
static const start = 'start:';
static const finish = 'finish:';
static const comment = 'comment:';
static const aiResponseLimit = 'AI_RESPONSE_LIMIT';
static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT';
static const aiMaxRequired = 'AI_MAX_REQUIRED:';
static const localAINotReady = 'LOCAL_AI_NOT_READY';
static const localAIDisabled = 'LOCAL_AI_DISABLED';
}
enum AiType {
cloud,
local;
bool get isCloud => this == cloud;
bool get isLocal => this == local;
}
class PredefinedFormat extends Equatable {
const PredefinedFormat({
required this.imageFormat,
required this.textFormat,
});
final ImageFormat imageFormat;
final TextFormat? textFormat;
PredefinedFormatPB toPB() {
return PredefinedFormatPB(
imageFormat: switch (imageFormat) {
ImageFormat.text => ResponseImageFormatPB.TextOnly,
ImageFormat.image => ResponseImageFormatPB.ImageOnly,
ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,
},
textFormat: switch (textFormat) {
TextFormat.paragraph => ResponseTextFormatPB.Paragraph,
TextFormat.bulletList => ResponseTextFormatPB.BulletedList,
TextFormat.numberedList => ResponseTextFormatPB.NumberedList,
TextFormat.table => ResponseTextFormatPB.Table,
_ => null,
},
);
}
@override
List<Object?> get props => [imageFormat, textFormat];
}
enum ImageFormat {
text,
image,
textAndImage;
bool get hasText => this == text || this == textAndImage;
FlowySvgData get icon {
return switch (this) {
ImageFormat.text => FlowySvgs.ai_text_s,
ImageFormat.image => FlowySvgs.ai_image_s,
ImageFormat.textAndImage => FlowySvgs.ai_text_image_s,
};
}
String get i18n {
return switch (this) {
ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),
ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),
ImageFormat.textAndImage =>
LocaleKeys.chat_changeFormat_textAndImage.tr(),
};
}
}
enum TextFormat {
paragraph,
bulletList,
numberedList,
table;
FlowySvgData get icon {
return switch (this) {
TextFormat.paragraph => FlowySvgs.ai_paragraph_s,
TextFormat.bulletList => FlowySvgs.ai_list_s,
TextFormat.numberedList => FlowySvgs.ai_number_list_s,
TextFormat.table => FlowySvgs.ai_table_s,
};
}
String get i18n {
return switch (this) {
TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(),
TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(),
TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(),
TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),
};
}
}

View file

@ -0,0 +1,181 @@
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:protobuf/protobuf.dart';
import 'package:universal_platform/universal_platform.dart';
typedef OnModelStateChangedCallback = void Function(AiType, bool, String);
typedef OnAvailableModelsChangedCallback = void Function(
List<AIModelPB>,
AIModelPB?,
);
class AIModelStateNotifier {
AIModelStateNotifier({required this.objectId})
: _localAIListener =
UniversalPlatform.isDesktop ? LocalAIStateListener() : null,
_aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) {
_startListening();
_init();
}
final String objectId;
final LocalAIStateListener? _localAIListener;
final AIModelSwitchListener _aiModelSwitchListener;
LocalAIPB? _localAIState;
AvailableModelsPB? _availableModels;
// callbacks
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
final List<OnAvailableModelsChangedCallback>
_availableModelsChangedCallbacks = [];
void _startListening() {
if (UniversalPlatform.isDesktop) {
_localAIListener?.start(
stateCallback: (state) async {
_localAIState = state;
_notifyStateChanged();
if (state.state == RunningStatePB.Running ||
state.state == RunningStatePB.Stopped) {
await _loadAvailableModels();
_notifyAvailableModelsChanged();
}
},
);
}
_aiModelSwitchListener.start(
onUpdateSelectedModel: (model) async {
final updatedModels = _availableModels?.deepCopy()
?..selectedModel = model;
_availableModels = updatedModels;
_notifyAvailableModelsChanged();
if (model.isLocal && UniversalPlatform.isDesktop) {
await _loadLocalAiState();
}
_notifyStateChanged();
},
);
}
void _init() async {
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
_notifyStateChanged();
_notifyAvailableModelsChanged();
}
void addListener({
OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
}) {
if (onStateChanged != null) {
_stateChangedCallbacks.add(onStateChanged);
}
if (onAvailableModelsChanged != null) {
_availableModelsChangedCallbacks.add(onAvailableModelsChanged);
}
}
void removeListener({
OnModelStateChangedCallback? onStateChanged,
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
}) {
if (onStateChanged != null) {
_stateChangedCallbacks.remove(onStateChanged);
}
if (onAvailableModelsChanged != null) {
_availableModelsChangedCallbacks.remove(onAvailableModelsChanged);
}
}
Future<void> dispose() async {
_stateChangedCallbacks.clear();
_availableModelsChangedCallbacks.clear();
await _localAIListener?.stop();
await _aiModelSwitchListener.stop();
}
(AiType, String, bool) getState() {
if (UniversalPlatform.isMobile) {
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
}
final availableModels = _availableModels;
final localAiState = _localAIState;
if (availableModels == null) {
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
}
if (localAiState == null) {
Log.warn("Cannot get local AI state");
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
}
if (!availableModels.selectedModel.isLocal) {
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
}
final editable = localAiState.state == RunningStatePB.Running;
final hintText = editable
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr();
return (AiType.local, hintText, editable);
}
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
final availableModels = _availableModels;
if (availableModels == null) {
return ([], null);
}
return (availableModels.models, availableModels.selectedModel);
}
void _notifyAvailableModelsChanged() {
final (models, selectedModel) = getAvailableModels();
for (final callback in _availableModelsChangedCallbacks) {
callback(models, selectedModel);
}
}
void _notifyStateChanged() {
final (type, hintText, isEditable) = getState();
for (final callback in _stateChangedCallbacks) {
callback(type, isEditable, hintText);
}
}
Future<void> _loadAvailableModels() {
final payload = AvailableModelsQueryPB(source: objectId);
return AIEventGetAvailableModels(payload).send().fold(
(models) => _availableModels = models,
(err) => Log.error("Failed to get available models: $err"),
);
}
Future<void> _loadLocalAiState() {
return AIEventGetLocalAIState().send().fold(
(localAIState) => _localAIState = localAIState,
(error) => Log.error("Failed to get local AI state: $error"),
);
}
}
extension AiModelExtension on AIModelPB {
bool get isDefault {
return name == "Auto";
}
String get i18n {
return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
}
}

View file

@ -0,0 +1,180 @@
import 'dart:async';
import 'package:appflowy/ai/service/ai_model_state_notifier.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'ai_entities.dart';
part 'ai_prompt_input_bloc.freezed.dart';
class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
AIPromptInputBloc({
required String objectId,
required PredefinedFormat? predefinedFormat,
}) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),
super(AIPromptInputState.initial(predefinedFormat)) {
_dispatch();
_startListening();
_init();
}
final AIModelStateNotifier aiModelStateNotifier;
@override
Future<void> close() async {
await aiModelStateNotifier.dispose();
return super.close();
}
void _dispatch() {
on<AIPromptInputEvent>(
(event, emit) {
event.when(
updateAIState: (aiType, editable, hintText) {
emit(
state.copyWith(
aiType: aiType,
editable: editable,
hintText: hintText,
),
);
},
toggleShowPredefinedFormat: () {
final showPredefinedFormats = !state.showPredefinedFormats;
final predefinedFormat =
showPredefinedFormats && state.predefinedFormat == null
? PredefinedFormat(
imageFormat: ImageFormat.text,
textFormat: TextFormat.paragraph,
)
: null;
emit(
state.copyWith(
showPredefinedFormats: showPredefinedFormats,
predefinedFormat: predefinedFormat,
),
);
},
updatePredefinedFormat: (format) {
if (!state.showPredefinedFormats) {
return;
}
emit(state.copyWith(predefinedFormat: format));
},
attachFile: (filePath, fileName) {
final newFile = ChatFile.fromFilePath(filePath);
if (newFile != null) {
emit(
state.copyWith(
attachedFiles: [...state.attachedFiles, newFile],
),
);
}
},
removeFile: (file) {
final files = [...state.attachedFiles];
files.remove(file);
emit(
state.copyWith(
attachedFiles: files,
),
);
},
updateMentionedViews: (views) {
emit(
state.copyWith(
mentionedPages: views,
),
);
},
clearMetadata: () {
emit(
state.copyWith(
attachedFiles: [],
mentionedPages: [],
),
);
},
);
},
);
}
void _startListening() {
aiModelStateNotifier.addListener(
onStateChanged: (aiType, editable, hintText) {
add(AIPromptInputEvent.updateAIState(aiType, editable, hintText));
},
);
}
void _init() {
final (aiType, hintText, isEditable) = aiModelStateNotifier.getState();
add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText));
}
Map<String, dynamic> consumeMetadata() {
final metadata = {
for (final file in state.attachedFiles) file.filePath: file,
for (final page in state.mentionedPages) page.id: page,
};
if (metadata.isNotEmpty && !isClosed) {
add(const AIPromptInputEvent.clearMetadata());
}
return metadata;
}
}
@freezed
class AIPromptInputEvent with _$AIPromptInputEvent {
const factory AIPromptInputEvent.updateAIState(
AiType aiType,
bool editable,
String hintText,
) = _UpdateAIState;
const factory AIPromptInputEvent.toggleShowPredefinedFormat() =
_ToggleShowPredefinedFormat;
const factory AIPromptInputEvent.updatePredefinedFormat(
PredefinedFormat format,
) = _UpdatePredefinedFormat;
const factory AIPromptInputEvent.attachFile(
String filePath,
String fileName,
) = _AttachFile;
const factory AIPromptInputEvent.removeFile(ChatFile file) = _RemoveFile;
const factory AIPromptInputEvent.updateMentionedViews(List<ViewPB> views) =
_UpdateMentionedViews;
const factory AIPromptInputEvent.clearMetadata() = _ClearMetadata;
}
@freezed
class AIPromptInputState with _$AIPromptInputState {
const factory AIPromptInputState({
required AiType aiType,
required bool supportChatWithFile,
required bool showPredefinedFormats,
required PredefinedFormat? predefinedFormat,
required List<ChatFile> attachedFiles,
required List<ViewPB> mentionedPages,
required bool editable,
required String hintText,
}) = _AIPromptInputState;
factory AIPromptInputState.initial(PredefinedFormat? format) =>
AIPromptInputState(
aiType: AiType.cloud,
supportChatWithFile: false,
showPredefinedFormats: format != null,
predefinedFormat: format,
attachedFiles: [],
mentionedPages: [],
editable: true,
hintText: '',
);
}

View file

@ -0,0 +1,204 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
import 'ai_entities.dart';
import 'error.dart';
enum LocalAIStreamingState {
notReady,
disabled,
}
abstract class AIRepository {
Future<void> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
List<AiWriterRecord> history = const [],
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) processMessage,
required Future<void> Function(String text) processAssistMessage,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
required void Function(LocalAIStreamingState state)
onLocalAIStreamingStateChange,
});
}
class AppFlowyAIService implements AIRepository {
@override
Future<(String, CompletionStream)?> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
List<AiWriterRecord> history = const [],
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) processMessage,
required Future<void> Function(String text) processAssistMessage,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
required void Function(LocalAIStreamingState state)
onLocalAIStreamingStateChange,
}) async {
final stream = AppFlowyCompletionStream(
onStart: onStart,
processMessage: processMessage,
processAssistMessage: processAssistMessage,
processError: onError,
onLocalAIStreamingStateChange: onLocalAIStreamingStateChange,
onEnd: onEnd,
);
final records = history.map((record) => record.toPB()).toList();
final payload = CompleteTextPB(
text: text,
completionType: completionType,
format: format?.toPB(),
streamPort: fixnum.Int64(stream.nativePort),
objectId: objectId ?? '',
ragIds: [
if (objectId != null) objectId,
...sourceIds,
].unique(),
history: records,
);
return AIEventCompleteText(payload).send().fold(
(task) => (task.taskId, stream),
(error) {
Log.error(error);
return null;
},
);
}
}
abstract class CompletionStream {
CompletionStream({
required this.onStart,
required this.processMessage,
required this.processAssistMessage,
required this.processError,
required this.onLocalAIStreamingStateChange,
required this.onEnd,
});
final Future<void> Function() onStart;
final Future<void> Function(String text) processMessage;
final Future<void> Function(String text) processAssistMessage;
final void Function(AIError error) processError;
final void Function(LocalAIStreamingState state)
onLocalAIStreamingStateChange;
final Future<void> Function() onEnd;
}
class AppFlowyCompletionStream extends CompletionStream {
AppFlowyCompletionStream({
required super.onStart,
required super.processMessage,
required super.processAssistMessage,
required super.processError,
required super.onEnd,
required super.onLocalAIStreamingStateChange,
}) {
_startListening();
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
void _startListening() {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
await _handleEvent(event);
},
);
}
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
Future<void> _handleEvent(String event) async {
// Check simple matches first
if (event == AIStreamEventPrefix.aiResponseLimit) {
processError(
AIError(
message: LocaleKeys.ai_textLimitReachedDescription.tr(),
code: AIErrorCode.aiResponseLimitExceeded,
),
);
return;
}
if (event == AIStreamEventPrefix.aiImageResponseLimit) {
processError(
AIError(
message: LocaleKeys.ai_imageLimitReachedDescription.tr(),
code: AIErrorCode.aiImageResponseLimitExceeded,
),
);
return;
}
// Otherwise, parse out prefix:content
if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) {
processError(
AIError(
message: event.substring(AIStreamEventPrefix.aiMaxRequired.length),
code: AIErrorCode.other,
),
);
} else if (event.startsWith(AIStreamEventPrefix.start)) {
await onStart();
} else if (event.startsWith(AIStreamEventPrefix.data)) {
await processMessage(
event.substring(AIStreamEventPrefix.data.length),
);
} else if (event.startsWith(AIStreamEventPrefix.comment)) {
await processAssistMessage(
event.substring(AIStreamEventPrefix.comment.length),
);
} else if (event.startsWith(AIStreamEventPrefix.finish)) {
await onEnd();
} else if (event.startsWith(AIStreamEventPrefix.localAIDisabled)) {
onLocalAIStreamingStateChange(
LocalAIStreamingState.disabled,
);
} else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) {
onLocalAIStreamingStateChange(
LocalAIStreamingState.notReady,
);
} else if (event.startsWith(AIStreamEventPrefix.error)) {
processError(
AIError(
message: event.substring(AIStreamEventPrefix.error.length),
code: AIErrorCode.other,
),
);
} else {
Log.debug('Unknown AI event: $event');
}
}
}

View file

@ -7,7 +7,7 @@ part 'error.g.dart';
class AIError with _$AIError {
const factory AIError({
required String message,
@Default(AIErrorCode.other) AIErrorCode code,
required AIErrorCode code,
}) = _AIError;
factory AIError.fromJson(Map<String, Object?> json) =>
@ -17,6 +17,8 @@ class AIError with _$AIError {
enum AIErrorCode {
@JsonValue('AIResponseLimitExceeded')
aiResponseLimitExceeded,
@JsonValue('AIImageResponseLimitExceeded')
aiImageResponseLimitExceeded,
@JsonValue('Other')
other,
}

View file

@ -0,0 +1,92 @@
import 'dart:async';
import 'package:appflowy/ai/service/ai_model_state_notifier.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_model_bloc.freezed.dart';
class SelectModelBloc extends Bloc<SelectModelEvent, SelectModelState> {
SelectModelBloc({
required AIModelStateNotifier aiModelStateNotifier,
}) : _aiModelStateNotifier = aiModelStateNotifier,
super(SelectModelState.initial(aiModelStateNotifier)) {
on<SelectModelEvent>(
(event, emit) {
event.when(
selectModel: (model) {
AIEventUpdateSelectedModel(
UpdateSelectedModelPB(
source: _aiModelStateNotifier.objectId,
selectedModel: model,
),
).send();
emit(state.copyWith(selectedModel: model));
},
didLoadModels: (models, selectedModel) {
emit(
SelectModelState(
models: models,
selectedModel: selectedModel,
),
);
},
);
},
);
_aiModelStateNotifier.addListener(
onAvailableModelsChanged: _onAvailableModelsChanged,
);
}
final AIModelStateNotifier _aiModelStateNotifier;
@override
Future<void> close() async {
_aiModelStateNotifier.removeListener(
onAvailableModelsChanged: _onAvailableModelsChanged,
);
await super.close();
}
void _onAvailableModelsChanged(
List<AIModelPB> models,
AIModelPB? selectedModel,
) {
if (!isClosed) {
add(SelectModelEvent.didLoadModels(models, selectedModel));
}
}
}
@freezed
class SelectModelEvent with _$SelectModelEvent {
const factory SelectModelEvent.selectModel(
AIModelPB model,
) = _SelectModel;
const factory SelectModelEvent.didLoadModels(
List<AIModelPB> models,
AIModelPB? selectedModel,
) = _DidLoadModels;
}
@freezed
class SelectModelState with _$SelectModelState {
const factory SelectModelState({
required List<AIModelPB> models,
required AIModelPB? selectedModel,
}) = _SelectModelState;
factory SelectModelState.initial(AIModelStateNotifier notifier) {
final (models, selectedModel) = notifier.getAvailableModels();
return SelectModelState(
models: models,
selectedModel: selectedModel,
);
}
}

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