Compare commits

..
223 Commits
Author SHA1 Message Date
Lucas.Xu f1ad03eaa9 chore: bump version 0.6.7 (#5949)
* chore: update collab version

* chore: update changelog

* chore: update notification reddot style

* fix: unable to fetch reminders

* chore: optimize notification red dot size
2024-08-13 15:03:57 +08:00
Lucas.Xu 17c9c9b556 chore: optimize recent section performance (#5948)
* fix: duplicated call for setting recent view

* chore: reduce recent update
2024-08-13 13:52:02 +08:00
Lucas.Xu 93f9a2cab1 feat: display no access page (#5941)
* feat: display no access page

* fix: optimize the primary rounded button
2024-08-13 09:32:22 +08:00
Annie 4b710527c9 chore: remove file upload limitation & add tooltip for search (#5944) 2024-08-12 22:24:24 +08:00
Nathan.fooo 6e26dc128c chore: fix message id (#5943) 2024-08-12 22:24:06 +08:00
Annie d3d9ab2fb0 chore: reset line height to null as default value (#5942) 2024-08-12 18:08:54 +08:00
Nathan.fooo 1db8480b75 chore: using write lock to fix acquire lock error (#5938) 2024-08-12 16:28:34 +08:00
AnnieandNathan.fooo 7e53b34484 chore: center confirm or cancel (#5936)
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-08-12 16:19:16 +08:00
Lucas.Xu 34465efc24 chore: replace all the buttons on settings page with the primary button style (#5937) 2024-08-12 16:03:52 +08:00
AnnieandLucas.Xu a29b170b13 chore: remove stability ai (#5927)
* chore: remove stability ai

* chore: remove stabilityAI widgets

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-08-12 15:46:56 +08:00
Mohammad ZolfaghariandLucas.Xu 14b60fb9b0 fix: list paste bug #5846 (#5917)
* fix: list paste bug #5846

selecting a single word on a bullet or number list and pasting it
from the clipboard would clear the whole text of that node and
replace it with clipboard text.

resolves #5846

* chore: update editor version

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-08-12 15:46:45 +08:00
Nathan.fooo 1c638dd930 chore: show reference sources on local ai response (#5933)
* chore: show reference sources on local ai response

* chore: clippy

* chore: clippy

* chore: update i18n

* chore: update client api
2024-08-12 15:43:17 +08:00
Sota 55a4810d60 chore: update translations with Fink 🐦 (#5932) 2024-08-12 15:12:52 +08:00
AnnieandLucas.Xu 2bc7875bdd chore: polish upload image menu (#5928)
* chore: polish upload image menu

* fix: text align issue

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-08-12 13:52:53 +08:00
Nathan.fooo f7adcae8ff chore: show attachment on local ai (#5929)
* chore: show attachment on local ai

* chore: fix compile
2024-08-12 09:21:44 +08:00
Nathan.fooo 23997e977c refactor: revamp file upload and fix partitial upload bugs (#5924)
* chore: upload chat file to local ai

* chore: async func

* chore: individual file progress

* chore: fix test

* chore: fix file upload
2024-08-11 20:39:25 +08:00
Lucas.Xu 3ff47b7e1e fix: link to two calendars in same doc may failed randomly (#5926)
* chore: udpate translation

* fix: editor loses focus randomly when interacting with certain databases

* fix: text align issues on Windows

* chore: update editor version
2024-08-11 20:18:35 +08:00
AnnieandLucas.Xu d33af70a5c chore: revamp editor icon (#5925)
* chore: revamp file block icon

* chore: revamp other blocks icon

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-08-11 20:18:23 +08:00
nathan 510752868d chore: fix flutter wanrings 2024-08-11 15:16:02 +08:00
nathan 42d1bb84c5 chore: fix compile 2024-08-11 15:10:30 +08:00
Annie 2debd0283c chore: revamp question bubble (#5923) 2024-08-11 15:02:29 +08:00
Nathan.fooo e87ade6b3f chore: update tabbar icon (#5921) 2024-08-10 21:06:54 +08:00
e2a923a796 chore: update editor version (#5918)
* chore: update editor version

* fix: new property field width on Mobile

* feat: support enter to insert a new line on mobile

* feat: optimzie callout style

* feat: add hover effect on share button

* chore: fix

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: nathan <nathan@appflowy.io>
2024-08-10 20:49:06 +08:00
Annieandnathan cd0f8d80e9 chore: replace icons (#5914)
Co-authored-by: nathan <nathan@appflowy.io>
2024-08-10 18:05:53 +08:00
Nathan.fooo 7abe9f4661 feat: show indicator when send chat message with attachment/mention etc (#5919)
* chore: adjust line height

* chore: send stream message

* chore: index file

* chore: clippy
2024-08-10 17:23:37 +08:00
Nathan.fooo 758c304a74 feat: support mention a document as context on local ai (#5913)
* feat: support mention a document as context on local ai

* chore: rename

* chore: fix test

* chore: fix test
2024-08-09 21:55:20 +08:00
f84473c857 chore: update German translations to state of v0.6.7 (#5848)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* fix: add empty itemSeven removed by Inlang

* fix: add empty itemSeven removed by Inlang

fix for turkish translation

* fix: empty translation for itemSeven

quickfix for the empty translation of itemSeven. This has been done the same way to resolve previous issues with InLang and empty translation strings.

* fix: empty translation for itemSeven in TR

quickfix for the empty translation of itemSeven. This has been done the same way to resolve previous issues with InLang and empty translation strings.

* fix: remove itemSeven completely

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* fix: resolve conflicts after merging main

* chore: update translations with Fink 🐦

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-08-09 21:51:23 +08:00
Lucas.Xu a26b2a356c feat: optimize the slash menu item name & icon (#5908)
* feat: optimize the slash menu item name & icon

* feat: optimize the toolbar item name & icon

* fix: integration test

* fix: replace unaligned icons
2024-08-09 21:50:47 +08:00
Annie 57bbb6cc41 chore: replace property icons in database (#5910) 2024-08-09 21:08:13 +08:00
Nathan.fooo e82edc0419 chore: polish UI and display attachment when using local ai (#5906)
* chore: polish UI

* chore: fix compile
2024-08-09 07:40:24 +08:00
Lucas.Xu f57297e76d fix: known issues in 0.6.7 (#5903)
* feat: add capitalize extension for string and apply it in emoji picker header and icon picker

* fix: adjust tooltip height calculation

* feat: add Streamline open source icons notice and link

* feat: enhance heading toolbar item to toggle heading level and cancel on same level selection

* feat: use home-3 as default space icon

* feat: use the first character of space name as icon if icon was removed

* chore: update hover effect for delete workspace button

* chore: optimize space icon on mobile

* fix: adjust chat ui on mobile

* fix: adjust default space icon on mobile
2024-08-08 19:31:10 +08:00
Lucas.Xu 7769034467 chore: add more logs in reminder bloc (#5860)
* chore: add more logs in reminder bloc

* Revert "chore: add more logs in reminder bloc"

This reverts commit 9d0bb8fb29.

* chore: add more logs in reminder bloc

* fix: unable to view reminders on Desktop

* fix: force refresh reminders

* chore: fix flutter analyze

* feat: support database reminder on Mobile

* chore: remove referenced database padding
2024-08-08 13:35:22 +08:00
Nathan.fooo a523b8ff90 chore: chat ui polish (#5902)
* chore: chat ui polish

* chore: fmt

* chore: clippy
2024-08-08 12:07:00 +08:00
Kilu.HeandLucas.Xu 7b7b907017 feat: support custom icon (#5869)
* feat: support custom icon

* fix: quote

* chore: join lines for icons.json

* fix: manual add .svg for icon

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-08-08 09:49:34 +08:00
Lucas.Xu 29b262a1c6 chore: optimize mobile ai chat page (#5897)
* feat: improve chat page UI on mobile

* feat: integrate add page menu into chat page on mobile

* fix: only display document view in @ menu
2024-08-08 09:49:08 +08:00
Nathan.fooo e28a251e72 chore: chat UI poblish (#5895)
* chore: update ui

* chore: update send state

* chore: workspace owner prompt

* chore: show other user

* chore: fix ui
2024-08-07 16:48:09 +08:00
Lucas.Xu 98b7882d43 feat: move all the heading toolbar items into a popup menu (#5890)
* chore: udpate translations

* feat: move all the heading items into a popup menu

* chore: add arrow down icon after heading toolbar items

* fix: compile

* chore: adjust heading toolbar style
2024-08-07 12:42:52 +08:00
Lucas.Xu e279ad1cc7 feat: optimize editor toolbar tooltip (#5889)
* chore: improve icon picker dark mode color

* feat: optimize editor toolbar tooltip style

* feat: customize markdown item tooltip message

* chore: find the tooltip by rich message

* feat: add hover effect in toolbar item

* feat: add hover effect in toolbar item

* chore: optimize hover color in light mode

* chore: fix integration test

* chore: optimize align & font toolbar item

* chore: fix integration test
2024-08-07 11:37:30 +08:00
Mathias Mogensen a798b037db fix: add back invitation success toast (#5875) 2024-08-06 10:10:06 +02:00
Nathan.fooo 5fbaf664ba chore: update translation (#5887) 2024-08-06 16:01:32 +08:00
Lucas.Xu 41a346c7ed chore: adjust legacy space icon (#5886) 2024-08-06 15:10:44 +08:00
Lucas.Xu aab942d163 fix: remove unused expanded widget (#5885)
* fix: remove unused expanded widget

* Revert "fix: remove unused expanded widget"

This reverts commit dcdc318ea2.

* fix: remove unused expanded widget
2024-08-06 14:19:44 +08:00
Nathan.fooo 5757cc9a1d chore: polish ui (#5883)
* chore: polish ui

* chore: clippy

* chore: fmt
2024-08-06 13:17:56 +08:00
Lucas.Xu 453e6309d5 feat: refactor space icon picker (#5878)
* feat: refactor space icon picker

* chore: optimize the _loadIconGroups function

* feat: refactor emoji picker

* feat: integrate icon picker into flowy_icon_emoji_picker

* feat: support searching icon

* feat: support displaying new icons

* fix: flutter analyze

* chore: join lines

* feat: support space icon in view title

* feat: support customzing icon when creating space or managing space

* feat: customize the emoji picker and icon picker padding

* feat: shuffle icon

* fix: expand popup menu font size

* fix: flutter integration test
2024-08-06 11:47:38 +08:00
Lucas.Xu 4041724980 fix: prevent shaking in nested lists deeper in AI message (#5877) 2024-08-06 11:22:40 +08:00
Nathan.fooo d378c456d4 feat: Implement menu on chat (#5879)
* chore: implement menu

* chore: gather metadata

* chore: update client api

* chore: stream metadata

* chore: save metadata

* chore: update client api

* chore: adjust UI

* chore: fmt
2024-08-06 07:56:13 +08:00
Lucas.Xu 0abf916796 feat: support pasting image from Slack (#5864) 2024-08-02 22:00:40 +08:00
Lucas.Xu 393850ae4b chore: expand inline menu width (#5867) 2024-08-02 22:00:28 +08:00
Lucas.Xu 46bad4e7e8 chore: optimize the workspace menu hover status (#5865) 2024-08-02 16:10:49 +08:00
Lucas.Xu e9fc003e10 fix: + button in the navigation bar doesn't work when the space is not initialized (#5862) 2024-08-02 13:11:52 +08:00
Kilu.He cb60488bbe fix: replace wasm with axios (#5856)
* fix: replace wasm with axios

* fix: login redirect

* fix: flag emoji on windows
2024-08-02 12:19:32 +08:00
Lucas.Xu 04556252e1 chore: optimzie emoji align on desktop (#5857) 2024-08-02 10:20:31 +08:00
Mathias Mogensen c2e8a12427 fix: select option chip size (#5859) 2024-08-01 23:28:05 +02:00
Nathan.fooo 73421e0d58 feat: suppport translate and summary using local ai (#5858)
* chore: fix ui

* chore: fix ui

* chore: rename

* chore: rename

* chore: rename

* chore: refactor database ai interface

* chore: support local ai for database

* chore: clippy
2024-08-01 23:13:35 +08:00
Lucas.Xu b9fd3701cd feat: improve reminder color in notification page (#5855)
* feat: improve reminder color

* fix: notification page doesn't update when switching workspace
2024-08-01 20:21:25 +08:00
Lucas.Xu 9fbba5fb60 feat: notification multiple select (#5847)
* chore: update editor version

* feat: support multi select notification items

* fix: flutter analyze

* feat: add navgation bar button

* feat: add multi select item

* feat: add multi choice in notification page

* feat: support multi choice

* chore: update icon

* feat: support open page from notification page

* chore: update version
2024-08-01 16:30:15 +08:00
Lucas.Xu 7261d1e8da chore: update rocksdb version 0.22.0 (#5826)
* chore: update rocksdb version 0.22.0

* chore: update collab

* chore: revert rust toolchain to 1.77.2
2024-08-01 15:36:59 +08:00
Lucas.Xu 27aac2b911 chore: optimize emoji align on mobile (#5852) 2024-08-01 15:36:21 +08:00
Kilu.He 0cd5af5ffa fix: add comment tip (#5854) 2024-08-01 14:15:38 +08:00
Kilu.He 87e950733f fix: react action bugs (#5851) 2024-08-01 13:07:12 +08:00
Kilu.He 2402b4c6f1 feat: support global comment on publish (#5834)
* feat: support duplicate UI on web

* fix: replace google svg

* fix: modified some copy

* fix: adjust modal position

* fix: upgrade wasm package

* fix: text overflow

* fix: global comments

* fix: replace appflowy icon

* fix: demond load outline

* fix: lazy load

* fix: close duplicate entry

* fix: ci error

* fix: modified comment styles

* fix: adjust space

* fix: easy find reply comment

* fix: calendar scroll bugs

* fix: image render

* fix: replace loading

* fix: issues of test session

* fix: fixed adding comment

* fix: database view name
2024-08-01 12:59:04 +08:00
Mathias Mogensen ed81a0aff2 feat: upload file in document (#5843)
* feat: upload file in document

* feat: add uploaded at & improvements

* fix: popover onOpen not triggered by manual show

* test: add basic file test

* test: fix and add rename test
2024-07-31 15:49:35 +02:00
Lucas.Xu dce9231118 fix: try to reopen the first workspace if the workspace deletion failed (#5844) 2024-07-31 17:52:36 +08:00
Lucas.Xu d1c1449cf6 feat: support notification on mobile (#5831)
* feat: add inbox/unread/archived tabs

* feat: dump notification info

* chore: add reminder bloc

* feat: support unread / archive notification tab

* feat: support archive all & mark all as read

* feat: add empty page

* chore: optimize gesture

* feat: add red dot above notification icon

* chore: optimize code logic

* feat: optimize tabbar animation

* fix: notification align issue

* fix: todo list icon align issue

* feat: disable emoji button inside callout in read-only mode

* feat: optimize icon size in editor

* chore: improve text color in dark mode
2024-07-31 15:15:15 +08:00
Nathan.fooo 7c3dd5375d chore: switch to appflowy ai if local ai is not enable (#5839)
* chore: switch to appflowy ai if local ai is not enable

* chore: clippy

* chore: fix enable bug

* chore: fix compile

* chore: clippy
2024-07-31 11:47:09 +08:00
Lucas.Xu 81532d014e chore: use light haptic feedback (#5838)
* chore: use light haptic feedback

* chore: optimize tabbar & side action animation
2024-07-30 19:53:11 +08:00
Nathan.fooo 735a09b333 chore: remove local ai (#5837)
* chore: remove local ai

* chore: flutter analyze
2024-07-30 19:44:08 +08:00
nathan d52a04a9e3 chore: update chat input hint 2024-07-30 17:40:58 +08:00
Lucas.Xu a14ced458c chore: bump version 0.6.6 (#5833) 2024-07-30 17:37:28 +08:00
Nathan.fooo 5250a151c8 feat: Chat app download (#5835)
* chore: downlaod chat app

* chore: download app

* chore: disable local ai in billing

* chore: remove watcher

* chore: flutter analyzer
2024-07-30 17:32:30 +08:00
Mathias Mogensen d5a5a64fcf fix: photo gallery improvements + launch review fixes (#5830)
* fix: photo gallery improvements

* fix: improve when to show billing/plan pages

* feat: add grid photo gallery layout

* fix: close inline actions menu
2024-07-30 15:28:40 +08:00
Lucas.Xu 175c90e379 chore: update collab 93ec9a38722d5ed0628e6d03a161e2de172967f3 (#5832) 2024-07-30 15:28:21 +08:00
Lucas.Xu 115ae27ab1 fix: missing system font display (#5825)
* fix: missing system font display

* fix: integration test

* fix: cloud integration test
2024-07-29 14:30:11 +08:00
Chen ☁️☁️ e908892b4a chore: update simplified Chinese translate (#5798) 2024-07-29 10:00:05 +08:00
hasanbeder 948d61a9e0 chore: update tr-TR.json (#5822)
chore: Update Turkish translations
2024-07-29 09:59:25 +08:00
Lucas.Xu b5c4786896 fix: 0.6.6 launch review issues (#5824)
* fix: only rebuild the sidebar header when needed

* fix: skip the null or empty awareness data

* fix: ai writer setState error

* chore: add arrow down icon when hovering on workspace button

* chore: update workspace button tooltip sytle

* chore: only show collapse all subpages button when then page is expanded

* chore: update pin color

* chore: fix integration test

* Revert "chore: fix integration test"

This reverts commit e02b237423.
2024-07-29 09:57:58 +08:00
Nathan.fooo d24f1c566a fix: chat bugs (#5823)
* chore: implement slash menu

* chore: show popup

* chore: single column

* chore: update appflowy editor

* chore: fix warns
2024-07-27 23:47:08 +08:00
Lucas.Xu ddf68b010d chore: improve UI design on mobile (#5816) 2024-07-27 21:05:51 +08:00
Nathan.fooo 22b108df70 chore: enable billing (#5818) 2024-07-26 12:03:29 +08:00
Nathan.fooo aff9a61919 chore: notify in release build (#5817) 2024-07-26 10:28:18 +08:00
Lucas.Xu 043cd3f3bb fix: chat messaage color doesn't update when switching theme (#5815) 2024-07-26 10:08:00 +08:00
Lucas.Xu 29fb4af40a chore: improve UI design on Desktop (#5792)
* fix: only show collapse page button when the children of the page is not emtpy

* chore: set minimum sidebar width to 268

* chore: replace space lock icon and pin & unpin icon

* chore: change divider color

* chore: update divider color

* chore: improve create space color

* feat: highlight delete button when hovering

* chore: update translations

* fix: icon align issue

* feat: highlight sidebar resizer when hovering

* feat: add border to popover

* feat: optimize scroll bar

* feat: improve scrollbar hover color

* feat: support creating a new page via cmd+n

* chore: improve scrollbar color

* feat: improve tooltip style

* chore: fix unit test

* chore: bump version 0.6.6
2024-07-26 09:49:13 +08:00
Nathan.fooo a2e211555e chore: chat with pdf ui (#5811)
* chore: chat with pdf ui

* chore: only enable local ai on macos

* chore: add todo

* chore: adjust UI

* chore: clippy
2024-07-26 07:58:54 +08:00
Mathias Mogensen d1af172fb7 feat: drop images into editor (#5813) 2024-07-26 00:08:55 +02:00
Mathias Mogensenandnathan 23b6f94e82 feat: photo gallery block + image improvements (#5803)
* feat: support multiple images in image block

* feat: support drop files on image placeholder

* fix: overflow in image placeholder

* chore: clean code

* feat: refactor to multi image block

* feat: drop image on gallery to add

* feat: add delete image inside interactive viewer

* fix: some mobile improvements

* fix: web ci

* test: fix tests after dialog changes

* test: add basic multi image block test

* test: add to test runner

* test: open interactive viewer

* fix: add delete index to callback

* test: add navigation next/previous

* ci: fix

* ci: fix

* ci: fix

* test: add network image + deletion tests

* fix: remove duplicates after merge

* test: add multi image insertion test

* ci: try

* ci: try

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-07-25 14:47:08 +02:00
Nathan.fooo 82fffba45a chore: document local ai (#5810)
* chore: document local ai

* chore: update ui

* chore: clippy
2024-07-25 19:41:16 +08:00
charles 0373088fb8 fix: defaultWindowSize is out of scope (#5795) (#5796) 2024-07-25 13:53:46 +08:00
Lucas.Xu 0da0f320b7 fix: the action menu is blocked by the navigation bar on Android (#5806) 2024-07-25 13:52:20 +08:00
Lucas.Xu 4abdcd9478 fix: integration test (reset to default location) (#5804)
* fix: integration test (reset to default location)

* fix: unit test (appflowy cloud anon user and then sign in)

* Revert "fix: unit test (appflowy cloud anon user and then sign in)"

This reverts commit 2635aa42f0.

* fix: unit test (appflowy cloud anon user and then sign in)
2024-07-25 12:02:18 +08:00
Nathan.fooo 1e54c2dc7c chore: only run appflowy cloud if it's not exist (#5799)
* chore: only run appflowy cloud if it's not exist

* chore: update

* chore: log error

* chore: fix sign out test

* chore: update test
2024-07-25 07:59:03 +08:00
Lucas.Xu 01747f13e8 chore: bump version 0.6.5 (#5800) 2024-07-24 14:53:56 +08:00
Nathan.foooandMathias Mogensen 4a5eda6eeb chore: enable billing (#5779)
* chore: enable billing

* chore: adjust bright mode UI

* chore: show corresponding error in sidebar

* chore: dismiss dialog in ai writter when hit ai response

* fix: improvements from test session

* chore: ai error message for database

* chore: different prompt for workspace owner

* feat: cancel plan survey

* chore: show ai repsonse limit on chat

* fix: sidebar toast after merge

* chore: remove unused debug print

* fix: popover close on action

* fix: minor copy changes

* chore: disable billing

* chore: disbale billing

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-07-24 14:23:09 +08:00
Lucas.Xu b5d799655a fix: delete reference databases in toggle list (#5782) 2024-07-24 11:24:33 +08:00
Nathan.fooo 43b250ec28 CI: use self-hosted runner if developer can create a branch in the up-stream repo (#5791)
* chore: use self hosted macos runner
2024-07-24 10:55:58 +08:00
Khor Shu Heng 20e82880ff chore: remove unused step in rust CI, allow cache on failure, show container logs (#5770) 2024-07-24 10:03:46 +08:00
Khor Shu Heng 661a0879c6 fix: Remove empty edit from read me json (#5769)
* fix: Remove empty edit from read me json

* fix: remove delta field entirely
2024-07-24 09:45:41 +08:00
Kilu.He 2fb18dd051 fix: to 16px (#5790)
* fix: to 16px

* fix: to 16px

* fix: to 14px
2024-07-23 17:48:51 +08:00
Kilu.He 6334255e15 fix: some style issues (#5783) 2024-07-23 10:21:50 +08:00
Lucas.Xu f66821715f fix: integration test failed (grid row detail page: hide and show hidden fields) (#5781) 2024-07-22 16:51:58 +08:00
620e027c3e feat: ai billing (#5741)
* feat: start on AI plan+billing UI

* chore: enable plan and billing

* feat: cache workspace subscription + minor fixes (#5705)

* feat: update api from billing

* feat: add api for workspace subscription info (#5717)

* feat: refactor and start integrating AI plans

* feat: refine UI and add business logic for AI

* feat: complete UIUX for AI and limits

* chore: remove resolved todo

* chore: localize remove addon dialog

* chore: fix spacing issue for usage

* fix: interpret subscription + usage on action

* chore: update api for billing (#5735)

* chore: update revisions

* fix: remove subscription cache

* fix: copy improvements + use consistent dialog

* chore: update to the latest client api

* feat: support updating billing period

* Feat/ai billing cancel reason (#5752)

* chore: add cancellation reason field

* fix: ci add one retry for concurrent sign up

* chore: merge with main

* chore: half merge

* chore: fix conflict

* chore: observer error

* chore: remove unneeded protobuf and remove unwrap

* feat: added subscription plan details

* chore: check error code and update sidebar toast

* chore: periodically check billing state

* chore: editor ai error

* chore: return file upload error

* chore: fmt

* chore: clippy

* chore: disable upload image when exceed storage limitation

* chore: remove todo

* chore: remove openai i18n

* chore: update log

* chore: update client-api to fix stream error

* chore: clippy

* chore: fix language file

* chore: disable billing UI

---------

Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg>
Co-authored-by: nathan <nathan@appflowy.io>
2024-07-22 15:43:48 +08:00
Kilu.He 864768b3ba fix: can not display rows when rows overthan 1000 (#5777) 2024-07-22 15:38:13 +08:00
Kilu.HeandLucas.Xu a8b4f22703 feat: support publish database on web (#5748)
* fix: view name

* fix: hidden login on web

* fix: database update bugs

* feat: support render database on web

* fix: loading

* fix: calendar width on mobile

* fix: calendar boder color

* fix: replace some icons

* fix: deal with visible view ids

* fix: filter error child

* fix: hide filters and sorts

* fix: the style of relation

* fix: throw error when apply fail

* fix: upgrade yjs

* fix: eslint errors

* fix: support group by checkbox

* fix: add shortcut to clear data

* fix: relation

* fix: relation

* fix: relation

* fix: relation

* fix: view meta

* fix: view meta

* fix: view meta

* fix: empty database block

* fix: 0716 bugs

* fix: add button to url cell

* fix: jest test

* fix: unit tests

* fix: lint

* fix: reduce database space

* fix: add after payment page

* fix: add spacing

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-22 13:37:27 +08:00
Lucas.Xu 432db0f6d5 feat: publish databse to Web (#5709)
* chore: refactor the publish code

* feat: integrate publish into database page

* feat: add publish database structure

* feat: add database row collab

* feat: publish the database row collabs

* chore: update collab

* chore: improve question bubble

* feat: publish the database relations

* fix: rust ci

* feat: select grid view to publish

* feat: unable to deselect the primary database

* feat: optimize the read recent views speed (#5726)

* feat: optimize the read recent views speed

* fix: order of recent views should be from the latest to the oldest

* chore: update translations

* fix: replace the unable to be selected icon

* feat: remove left padding of inline database

* fix: code review

* chore: remove publish api err log

* chore: read the database collab and document collab from disk instead of memory

* chore: code cleanup

* chore: revert beta.appflowy.com

* chore: code cleanup

* test: add database encode test

* test: add publish database test

* chore: refresh sidebar layout

* chore: update comments
2024-07-22 13:35:42 +08:00
Abhijeet SinghandLucas.Xu e8e4162a5c chore: update Spanish translations (#5742)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-22 10:15:50 +08:00
sayishiandLucas.Xu 241234726f chore: update Chinese translations (#5727)
* chore: update translations with Fink 🐦

* chore: revert cargo.lock changes

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-22 10:07:50 +08:00
Khor Shu Heng e426970eed fix: Add retry for admin client sign in for test (#5767) 2024-07-22 10:02:01 +08:00
Yaron ShahrabaniandLucas.Xu d5544af6c5 chore: update Hebrew translation (#5738)
* Added Hebrew initial infra.

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* Hebrew translation.

* Hebrew update.

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-22 10:00:24 +08:00
Stefan Weiberg 9361afd573 chore: update German translations (#5722) 2024-07-19 16:40:03 +08:00
Artjom Jemeljanov dc93a336d6 chore: update Russian translations (#5730)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* Update en.json

* Update ru-RU.json

* Update ru-RU.json

* chore: update translations with Fink 🐦

* Update settings.json

* Update settings.json
2024-07-19 16:26:00 +08:00
Nathan.fooo 4e5482488f chore: update build config (#5759)
* chore: update build config

* chore: fix ci

* chore: update client-api

* chore: fix compile
2024-07-19 12:23:51 +08:00
Nathan.fooo 5bbf174ffd chore: enable local ai and local ai chat (#5755)
* chore: enable local ai and local ai chat

* chore: config for chat with file

* chore: flutter anaylzer
2024-07-18 20:54:35 +08:00
Lucas.Xu f36e3ae378 chore: bump version 0.6.4 (#5744) 2024-07-16 14:51:00 +08:00
Mathias MogensenandLucas.Xu dc6349b4b1 fix: improve color selector (#5743)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-16 14:50:50 +08:00
Lucas.Xu 44d8def3ce fix: reset space relationship when clearing cache (#5737)
* fix: space's child views didn't update when moving a page into it

* chore: remove check logic

* feat: integrate fix space into clear cache

* fix: translation missing value

* chore: update logger version
2024-07-16 13:23:07 +08:00
Nathan.fooo c6ad57f11d chore: show plugin state (#5740)
* chore: show plugin state

* chore: flutter analyzer

* chore: update
2024-07-15 22:45:53 +08:00
Nathan.fooo ff23165d3e chore: download llm files (#5723)
* chore: download file

* chore: config download ui

* chore: update zip

* chore: config download ui

* chore: unzip file

* chore: unzip file

* chore: rename

* chore: disable local ai

* chore: fmt

* chore: fix warning

* chore: update

* chore: fix clippy
2024-07-15 15:23:23 +08:00
Lucas.Xu 253e7597c4 feat: optimize the read recent views speed (#5726)
* feat: optimize the read recent views speed

* fix: order of recent views should be from the latest to the oldest
2024-07-15 14:23:20 +08:00
Zack d2e3fdfefd chore: fix compile (#5733) 2024-07-15 09:01:34 +08:00
Lucas.Xu a2336f5bf7 chore: improve popup menu color (#5725)
* chore: improve popup menu color

* fix: 100% text wrap into a new line in checklist cell
2024-07-12 15:23:22 +08:00
Lucas.Xu 8833df1740 fix: ai chat result contains html escape text (#5721)
* fix: ai chat result contains html escape text

* feat: support copy the code block from ai chat page

* feat: support copy code block from ai chat page

* fix: add hover effect to color picker button

* chore: increase minimum window size
2024-07-11 21:42:02 +08:00
Lucas.Xu 1524b10a8a chore: bump version 0.6.4 (#5719)
* chore: upgrade android target sdk to 34

* chore: downgrade macos builder version to support x86_64

* chore: bump version 0.6.4
2024-07-11 13:46:28 +08:00
Kilu.He fe0fa9b530 feat: support sign-in and sign-up on Web (#5712) 2024-07-11 12:55:22 +08:00
Lucas.Xu 80afcf44c0 fix: syntax error when generating translation files (#5718) 2024-07-11 11:56:00 +08:00
e500c89978 feat: choose cursor/selection color from palette or color picker #5041 (#5677)
* chore: show ai service error (#5675)

* feat: Implement color picker dialog for user color selection (#5041)

- Added `flex_color_picker` dependency.
- Implemented a new color picker dialog with the following features:
  - Display Material predefined colors.
  - Include primary, accent, and shade colors, as well as a color wheel for selection.
  - Add a graphical opacity selector.
  - Enhanced the previous dialog to include an icon in a text field for opening the palette.
- Added `suffixIcon` parameter to the `_ColorSettingTextField` class, making it reactive to tap gestures.
- Utilized `ColorExtension` on the `Color` class to avoid namespace conflicts when converting colors to hexadecimal strings.
- Added translation strings to english

This commit addresses issue #5041

* feat: Implement color picker dialog for user color selection (#5041)

- Added `flex_color_picker` dependency.
- Implemented a new color picker dialog with the following features:
      - Display Material predefined colors.
      - Include primary, accent, and shade colors, as well as a color wheel for selection.
      - Add a graphical opacity selector.
      - Enhanced the previous dialog to include an icon in a text field for opening the palette.
- Added `suffixIcon` parameter to the `_ColorSettingTextField` class, making it reactive to tap gestures.
- Utilized `ColorExtension` on the `Color` class to avoid namespace conflicts when converting colors to hexadecimal strings.
- Added translation strings to english

This commit addresses issue #5041

* chore: refactor code

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-10 17:49:59 +08:00
Lucas.Xu 079b9888a8 feat: render ai text message with appflowy_editor (#5682)
* feat: render ai text message with appflowy_editor

* chore: update appflowy_editor

* fix: integration test

* feat: support paste inAppJson format

* chore: update appflowy_editor
2024-07-10 17:17:07 +08:00
Lucas.Xu 0fe383e538 feat: enable debug logs in internal build (#5713) 2024-07-10 13:55:40 +08:00
Bartosz Sypytkowski c006e29afc chore: update yrs and appflowy collab dependencies (#5707)
* chore: update yrs and appflowy collab dependencies

* chore: fix clippy errors

* chore: update yrs and collab dependencies in appflowy tauri

* chore: update yrs and collab dependencies in appflowy web app
2024-07-10 10:10:21 +08:00
Lucas.Xu 655a2b7093 chore: bump version 0.6.3 (#5701) 2024-07-08 13:46:27 +08:00
23c67bcdba feat: support publish document (#5576)
* feat: support a event for getting encoded collab of document

* feat: support publish view and unpublish views

* feat: publish page to the web

* chore: refacotor share bloc

* feat: call the publish event

* feat: support publish view and unpublish views

* feat: integrate publish api

* feat: integrate unpublish api

* feat: fetch the publish info to show the publish status

* feat: support publish interfaces

* fix: lint error

* fix: modified web server

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* chore: refactor publish bloc

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: the name is too long to publish

* chore: change color

* fix: some style

* fix: some style

* feat: refacotor share menu UI

* fix: some style

* fix: lint

* fix: some style

* feat: refacotor export-as

* fix: some style

* chore: refactor share menu colors

* fix: rust ci

* fix: some style

* fix: some style

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: rerelease

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: og image

* fix: support copy button

* fix: support copy button

* fix: support copy button

* chore: add a params

* feat: use default publish name

* chore: update copy

* feat: show a confirm deletion dialog if the deleted page contains published page

* feat: add copy toast in publish tab

* fix: to 404

fix: to 404

fix: to 404

fix: the error to 404

* feat: unpublish the page auto when moving it to another space

* feat: improve confirm deletion dialog

* feat: show unpublish error

* chore: use beta.appflowy.com

* feat: disable publish in non-apppflowy-cloud user mode

* fix: modified bullted icon style

* fix: the dark mode color

* fix: save the dark mode in local storage

* fix: text color

* chore: make bash script more portable (#5679)

* fix: title longer

* chore: move the files and modified the en

* chore: update deploy.sh

* chore: modified Dockerfile

* chore: modified server.cjs to server.js

* chore: modifed server.js to server.ts

* chore: replace publish url

* chore: remove todo list hover

* chore: show confirm dialog before deleting page

* fix: unpublish the pages before deleting

* fix: table cell bg color

* fix: callout icon

* fix: list number

* fix: emoji

* fix: number icon

* fix: callout icon position

* fix: add margin bottom

* fix: code block

* fix: support scroll for breadcrumbs

* fix: the breadcrumb doesn't update after moving page

* fix: 0705 issues

* fix: update publish status afer deleting page

* chore: add hover effect for visit site button

* fix: remove puiblish url text field enable border color

* chore: update delete page copy

* chore: enable debug category

* fix: only render sidebar if the spaces are ready

* fix: the breadcrumb doesn't update after moving page

* fix: auto code

* fix: add emoji

* fix: add emoji

* fix: favicon

* fix: cypress test

* fix: remove deploy ci

* fix: default url

* chore: revert launch.json

* fix: docker ci

* fix: change favicon

* fix: flutter integration test

* feat: add hover effect to share menu

* chore: add a checkmark if the page has been published

* chore: revert space deletion

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Zack <speed2exe@live.com.sg>
2024-07-08 13:45:57 +08:00
Nathan.fooo 521fffd97c refactor: extract chat plugin to new repo (#5699)
* chore: update ui

* chore: update

* chore: update

* chore: separate to new crate

* chore: update commit id

* chore: fix compile

* chore: bump version
2024-07-08 13:19:13 +08:00
Lucas.Xu 2ecc2a67a9 fix: take the max value of the keyboard height and the view insets bottom to make the toolbar visible (#5700)
* fix: android toolbar will be blocked if the scroll bar is hidden

* fix: last opened view is synced in same workspace

* fix: fallback to space if the section sidebar contains space

* chore: revert last opened view code

* fix: integration tests
2024-07-08 13:15:42 +08:00
Lucas.Xu 2d11215bb2 chore: use latest macos runner to build the release package (#5686) 2024-07-08 09:43:23 +08:00
Lucas.Xu 6d0c9f766b fix: filter chat page when duplicating (#5676)
* fix: filter chat page when duplicating

* fix: don't clear selection if it has been disposed
2024-07-02 14:35:29 +08:00
Nathan.fooo 7a3f013a87 chore: show ai service error (#5675) 2024-07-02 13:26:53 +08:00
Lucas.Xu 8c1520b273 feat: sync the created view after duplicating (#5674)
* feat: sync the created view after duplicating

* chore: revert launch.json

* chore: refacotor code
2024-07-02 13:02:15 +08:00
Mathias MogensenandLucas.Xu a7b850e752 chore: disable cloud search (#5663)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-02 13:02:01 +08:00
Lucas.Xu 42dc6017c6 feat: support moving page to a space (#5665)
* feat: support moving page to a space

* chore: update collab repo
2024-07-02 10:29:46 +08:00
Stefan Weiberg f9fba6b561 chore: update German translations (#5640) 2024-07-02 09:44:16 +08:00
Nathan.fooo 46896b5a2b chore: remove appflowy_web project (#5671)
* chore: remove appflowy_web project

* chore: remove ci
2024-07-01 22:49:24 +08:00
Lucas.Xu 2b8dca209e feat: sync the documents and databases after batch importing documents and databases (#5644)
* feat: support batch import

* feat: support batch import database

* chore: revert launch.json

* chore: fix rust ci

* fix: rust ci
2024-07-01 14:44:08 +08:00
Lucas.Xu c78f23e1c0 fix: v0.6.2 issues (#5654)
* fix: remove create button in move to menu

* fix: add loading indicator when duplicating space

* fix: sidebar header expand icon status

* fix: text within select tag overflow

* fix: callout block icon align issue

* feat: sync the space icon when creating a space

* fix: duplicated hover views

* fix: cover image doesn't update
2024-07-01 14:43:57 +08:00
Lucas.Xu 50f5be3e75 chore: fix rustfmt ci test (#5664) 2024-07-01 13:26:43 +08:00
Nathan.fooo e1c68c1b72 feat: Run Local AI model in AppFlowy (#5655)
* chore: load plugin

* chore: sidecar

* chore: fix test

* chore: clippy

* chore: save chat config

* chore: arc plugin

* chore: add plugins

* chore: clippy

* chore: test streaming

* chore: config chat

* chore: stream message

* chore: response with local ai

* chore: fix compile

* chore: config ui

* chore: fix load plugin

* chore: add docs

* chore: update docs

* chore: disable local ai

* chore: fix compile

* chore: clippy
2024-06-30 17:38:39 +08:00
Mathias Mogensen 3bcadff152 fix: settings changes + ai layout improvements (#5656)
* fix: settings changes

* fix: improve chat layout
2024-06-28 22:54:54 +08:00
Mathias Mogensen b09ff040f1 fix: postpone fetching member (#5648) 2024-06-28 09:03:30 +08:00
Nathan.fooo bf087bba5d fix: score filter (#5645)
* fix: score filter

* chore: update score limit
2024-06-28 07:57:12 +08:00
Lucas.Xu 605a53f2ae feat: open the duplicated space by default (#5638) 2024-06-27 14:12:52 +08:00
Stefan Weiberg ecc5555a4e chore: update German translations (#5630) 2024-06-27 13:34:51 +08:00
Lucas.Xu d2a5bab6d9 chore: bump version 0.6.2 (#5637) 2024-06-27 12:02:15 +08:00
Lucas.Xu a1dec0f269 feat: add check workspace health button in settings page (#5631) 2024-06-27 09:57:06 +08:00
Mohammad Zolfaghari b24fbc6b60 fix: fill field_settings on creating linked view (#5598) 2024-06-27 03:07:08 +02:00
Mathias Mogensen bbb3f95a13 fix: billing+cleanup (#5634)
* fix: billing fixes

* chore: best practice and cleaning

* chore: dispose of unused resources

* chore: upgrade editor version
2024-06-27 02:00:21 +02:00
Mathias Mogensen 7859fc7922 fix: macos fullscreen window position (#5632) 2024-06-27 00:36:59 +02:00
Lucas.Xu f812040f04 feat: support creating database at the first level (#5627)
* feat: support creating database at the first level

* chore: add loading indicator when importing file
2024-06-26 13:27:33 +08:00
Lucas.Xu 7d96c2dfd4 feat: add key + as zoom-in key (#5628) 2024-06-26 13:27:13 +08:00
Mathias Mogensen e4132ed217 chore: enable cloud search (#5624)
* chore: enable cloud search

* fix: filter received msgs by query
2024-06-25 21:56:58 +02:00
Mathias Mogensen a7f40b2adc fix: paste in list (#5621)
* fix: support pasting in list

* test: add simple test

* chore: remove debugPrint
2024-06-25 20:08:01 +02:00
Mathias Mogensen c996c9c28e fix: maximized window optimization (#5622) 2024-06-25 20:07:43 +02:00
MarcinandLucas.Xu f59b8a2e5c chore: update Polish translations (#5593)
* chore: update translations with Fink 🐦

* Revert "chore: update translations with Fink 🐦"

This reverts commit e3b7ca9e22.

* chore: update pl-PL translations

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 21:16:35 +08:00
JakobandLucas.Xu f7480cba16 chore: update de-DE translations (#5588)
* chore: update translations with Fink 🐦

* Revert "chore: update translations with Fink 🐦"

This reverts commit f24a479b63.

* chore: update de-DE translations

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 21:09:02 +08:00
Mathias Mogensen 271b250eac fix: make theme uploads more tolerant (#5617) 2024-06-25 13:52:59 +02:00
Nathan.foooandLucas.Xu 6b1e7b6ac8 chore: set document id (#5585)
* chore: set document id

* feat: set document id to upload image api

* chore: fmt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 17:36:41 +08:00
Lucas.Xu bf2a00b133 fix: prevent overfow of space name and view name (#5619)
* fix: space name overflow

* fix: space name overflow in space menu

* fix: view name overflow in favorite menu
2024-06-25 17:36:00 +08:00
Lucas.Xu a8ed93054c feat: support moving page across spaces (#5618)
* feat: support moving page across spaces

* feat: refacotor move api

* feat: filter the database views

* feat: support searching in move page menu
2024-06-25 16:43:58 +08:00
Lucas.Xu b9ad2768cf feat: duplicate space (#5612) 2024-06-25 10:03:02 +08:00
54c9d12171 feat: support switch model (#5575)
* feat: ai settings page

* chore: intergate client api

* chore: replace open ai calls

* chore: disable gen image from ai

* chore: clippy

* chore: remove learn about ai

* chore: fix wanrings

* chore: fix restart button title

* chore: remove await

* chore: remove loading indicator

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 07:59:38 +08:00
Lucas.Xu 40312f4260 fix: infinite loading (#5613) 2024-06-24 20:14:22 +08:00
Zack d2ca41c8f5 Remove deprecated add member (#5611)
* chore: remove deprecated add member directly

* chore: cargo clippy

* chore: cargo clippy

* chore: cargo clippy
2024-06-24 14:19:36 +08:00
Mathias Mogensen 7586a0ed48 fix: settings ui improvements (#5590)
* fix: settings ui improvements

* fix: replace placeholder + fix open folder

* fix: collapse sidebar button on windows

* test: fix tests after changes

* test: missed type cast
2024-06-20 21:01:20 +02:00
Mathias MogensenandLucas.Xu d8b32168a9 feat: hide application to dock macos (#5592)
* feat: hide application to tray macos

* chore: enable reopen the app after closing it

* fix: remove tray

* fix: remove asset

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-20 21:00:56 +02:00
Mathias Mogensen 8ca541bfa1 fix: reminder race condition (#5587) 2024-06-20 10:13:59 +02:00
Nathan.fooo b64da2c02f refactor: File upload (#5542)
* chore: rename service

* refactor: upload

* chore: save upload meta data

* chore: add sql test

* chore: uploader

* chore: fix upload

* chore: cache file and remove after finish

* chore: retry upload

* chore: pause when netowork unreachable

* chore: add event test

* chore: add test

* chore: clippy

* chore: update client-api commit id

* chore: fix flutter test
2024-06-20 07:44:57 +08:00
Lucas.Xu fdaca36b87 fix: automatically open page after switching space (#5580) 2024-06-19 21:28:49 +08:00
Lucas.Xu 8557383a89 fix: cursor height is inconsistent after upgrading to Flutter 3.22 (#5574)
* fix: cursor height is inconsistent after upgrading to Flutter 3.22

* fix: cursor height is inconsistent after upgrading to Flutter 3.22

* fix: android toolbar height issue
2024-06-19 19:34:47 +08:00
Mathias Mogensen fa86480458 feat: shortcuts page remake (#5567)
* feat: settings shortcuts page remake

* test: add shortcut test

* fix: physics on listview

* fix: menu icon
2024-06-19 10:24:34 +02:00
Nathan.fooo d75eb05707 chore: fix flutter test (#5572) 2024-06-19 13:26:18 +08:00
Lucas.Xu a2b32585a1 chore: bump version 0.6.0 (#5571) 2024-06-19 12:10:05 +08:00
Lucas.Xu 75cea400d2 fix: cursor jump randomly in check list item (#5565)
* chore: remove debug logs

* fix: cursor jump ramdomly in checklist item
2024-06-19 09:33:27 +08:00
Lucas.XuandMathias Mogensen ed82ec8eef chore: remove gl_es env (#5564)
* fix: try without media kit

* chore: remove gl_es env

* Revert "fix: try without media kit"

This reverts commit e331dbfabf.

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-06-18 16:16:31 +08:00
Lucas.XuandMathias Mogensen b6debd6c71 fix: remove media_kit (#5563)
Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-06-18 15:47:52 +08:00
Nathan.fooo a05ed63a3e chore: fix can't delete characters in card (#5562)
* chore: fix can't delete characters in card

* chore: i18n
2024-06-18 13:12:57 +08:00
Nathan.fooo 3e75f1f24a chore: fix checklist potential panic (#5561)
* chore: fix checklist

* chore: fix checklist
2024-06-18 10:16:39 +08:00
Lucas.Xu e607694729 chore: change slash menu hover color (#5555)
* feat: add a pinned icon after page name

* chore: change slack menu hover color

* Revert "feat: add a pinned icon after page name"

This reverts commit a668e0bd09.
2024-06-18 09:55:43 +08:00
ansizheshi 91d870415e chore: update zh-CN translations (#5545)
* chore: update translations with Fink 🐦

* Delete frontend/resources/translations/de-DE.json

* Add files via upload

* Restore en.json

* Restore en.json
2024-06-18 09:52:44 +08:00
Nathan.fooo 21a497924c chore: adjust height (#5558) 2024-06-17 21:38:05 +08:00
Mathias Mogensen 4a126e17ce fix: settings improvements (#5547)
* fix: user workspace bloc changed

* fix: use translate card cell style

* fix: add getworkspacemember

* fix: billing launch review

* fix: disable time field

* fix: member tooltip

* fix: remove my account description

* fix: punctuation

* fix: filter workspace font

* fix: cloud toggle

* fix: minor adjustments

* chore: disable cloud document search

* fix: improve workspace name textfield

* test: move billing test to cloud

* fix: use cache over remote

* fix: clippy and tests

* chore: amend flowy tooltip

* test: add pump and settle

* test: integration test for local auth
2024-06-17 14:30:19 +02:00
Lucas.Xu 8bf97ad5c6 feat: add a pinned icon after page name (#5553) 2024-06-17 19:18:06 +08:00
Lucas.Xu a85d11fb6d feat: collapse space and switch space by shortcuts (#5554)
* chore: improve delete popup style

* feat: support collapsing space and switching space by shortcuts
2024-06-17 18:59:08 +08:00
Lucas.Xu 1fd34bc58a chore: turn off the billing feature (#5551) 2024-06-17 17:33:40 +08:00
Lucas.Xu 70a7b856a2 feat: improve space UX (#5549)
* fix: scroll bar hover color

* chore: replace board icon

* chore: remove close button in upgrade menu

* chore: add translation

* feat: switch to the next space by shortcut

* chore: replace space lock icon

* chore: remove arrow icon in change icon button

* feat: only show other favorites header if the other two is empty

* fix: create new page in current space

* feat: create a new page in a newly created space by default

* chore: update translations

* feat: open the first page after switching space by default

* chore: replace board icon

* fix: open the first page(non-space) after switching workspace

* chore: revert more icon

* chore: revert editor version
2024-06-17 16:23:17 +08:00
Lucas.Xu 49683e6079 fix: migrate the private views only if they are not empty (#5544) 2024-06-17 10:29:15 +08:00
Lucas.Xu 13107b63e9 feat: improve upgrade space toast (#5543)
* fix: divider color

* feat: improve upgrade space toast
2024-06-15 09:03:09 +08:00
Nathan.fooo ce1a6e4fca chore: highlight code block (#5540) 2024-06-14 14:53:53 +08:00
Lucas.Xu 1d23e28eaf chore: enable billing system (#5539) 2024-06-14 08:42:20 +02:00
Lucas.Xu 2a4d78b688 feat: improve hover color in dark mode (#5538) 2024-06-14 14:31:55 +08:00
Lucas.Xu cbe452a73d feat: improve page thumbnail on mobile (#5535)
* feat: improve view thumbnail

* chore: blur the bottom navigation bar

* feat: improve date format

* feat: support url avatar

* fix: remove duplicated divider
2024-06-14 13:15:41 +08:00
Nathan.fooo 27899ee993 chore: save workspace member info to disk (#5536)
* chore: save workspace member info to disk

* chore: fix clippy
2024-06-14 11:34:23 +08:00
Lucas.Xu 785597f53e fix: space issues (#5531)
* fix: display the space icon in breadcrumb and disable the space entry

* fix: the icon and the title are not aligned center in more menu

* fix: incorrect space icon corner radius

* fix: missed tooltip for add new page button

* fix: disable space migration for local user

* chore: use general as default space name

* fix: space name overflow

* fix: only show arrow button when hovering on page

* fix: sidebar tooltip font size

* fix: use space name as hint text

* feat: support adding a new space from space menu

* fix: filter the space view
2024-06-14 09:32:02 +08:00
Nathan.fooo e2ce274718 chore: polish chat UI (#5533)
* chore: adjust chat ui

* chore: clippy
2024-06-14 09:02:06 +08:00
Zack 6be9c00127 chore: update to latest client api (#5529) 2024-06-13 15:14:20 +08:00
Mohammad ZolfaghariandMathias Mogensen aa621a8d84 feat: timer field (#5349)
* feat: wip timer field

* feat: timer field fixing errors

* feat: wip timer field frontend

* fix: parsing TimerCellDataPB

* feat: parse time string to minutes

* fix: don't allow none number input

* fix: timer filter

* style: cargo fmt

* fix: clippy errors

* refactor: rename field type timer to time

* refactor: missed some variable and files to rename

* style: cargo fmt fix

* feat: format time field type data in frontend

* style: fix cargo fmt

* fix: fixes after merge

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
2024-06-13 08:52:13 +02:00
Mathias Mogensen 2d4300e931 chore: update version to 0.6.0 (#5528) 2024-06-13 08:25:11 +02:00
Lucas.Xu dc12938ab6 feat: support switching space on mobile (#5527) 2024-06-13 14:14:18 +08:00
Lucas.Xu 2d674060c6 feat: introduce space to manage the pages (#5517)
* fix: resizing icon on mobile

* feat: add space feature

* feat: support creating space

* feat: support creating new space

* feat: support space expand status

* feat: support creating page in space

* feat: support customizing space icon

* feat: display the space icon on space menu

* feat: add space more action button

* fix: flutter analyze

* feat: support editing space icon on more menu

* chore: update translations

* feat: manage space

* feat: delete workspace

* feat: disable delete button if needed

* feat: add private lock

* chore: adjust the old version

* feat: display upgrade button

* feat: support migrating space

* feat: support migrating space

* feat: allow user to upgrade space maunally

* fix: dark mode issue

* fix: create space delay

* chore: translations

* chore: disable workspace test
2024-06-13 13:43:29 +08:00
Nathan.fooo ecca81f3b8 chore: exclude ai columns (#5526) 2024-06-13 11:15:59 +08:00
Lucas.Xu 15329b39d0 chore: update editor version (#5513) 2024-06-13 07:58:05 +08:00
Mathias Mogensen bd5f5f8b9e feat: integrate cloud document search (#5523) 2024-06-13 01:37:19 +02:00
4f4be7eac7 feat: enable group condition (#5248)
* feat: enable group condition

* style: added i18n for date field group conditions

* fix: flutter analyze

* fix: test use i18n

* fix: more localization

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-06-13 00:02:16 +02:00
Nathan.fooo f1a4deb459 chore: support more language (#5522)
* chore: support more language

* chore: adjust ui
2024-06-12 23:17:57 +08:00
Mathias MogensenandZack Fu Zi Xiang 4708c0f779 feat: plan+billing (#5518)
* feat: billing client

* feat: subscribe workspace default impl

* feat: added create subscription

* feat: add get workspace subs

* feat: added subscription cancellation

* feat: add workspace limits api

* fix: update client api

* feat: user billing portal

* feat: billing UI (#5455)

* feat: plan ui

* feat: billing ui

* feat: settings plan comparison dialog

* feat: complete plan+billing ui

* feat: backend integration

* chore: cleaning

* chore: fixes after merge

* fix: dependency issue

* feat: added subscription plan cancellation information

* feat: subscription callback + canceled date

* feat: put behind feature flag

* feat: downgrade/upgrade dialogs

* feat: update limit error codes

* fix: billing refresh + downgrade dialog

* fix: some minor improvements to settings

* chore: use patch for client-api in tauri

* fix: add shared-entity to patch

* fix: compile

* ci: try to add back maximize build space step

* test: increase timeout in failing test

---------

Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg>
2024-06-12 17:08:55 +02:00
Nathan.fooo 3d7a500550 feat: AI translation in Database (#5515)
* chore: add tranlate field type

* chore: integrate ai translate

* chore: integrate client api

* chore: implement UI
2024-06-12 16:32:28 +08:00
Mathias Mogensen 815c99710e feat: magic link sign-in as anonymous user (#5520)
* feat: magic link sign-in as anonymous user

* chore: remove commented code

* fix: custom sign-in dialog

* fix: bring back sign-out dialog

* fix: add minor space for sync indicator
2024-06-11 22:17:29 +02:00
Murat Mirgün ERCAN c9fe93920f chore: update tr_TR translations (#5499) 2024-06-11 10:17:28 +08:00
Lucas.Xu 5012b99c9b fix: remove member request on launch (#5505) 2024-06-11 10:16:32 +08:00
Mathias Mogensen 6b46372aa0 fix: custom font dropdown (#5502)
* fix: custom font dropdown

* fix: dart analyzer
2024-06-10 15:23:26 +02:00
Mathias Mogensen b6d873db1b chore: remove build bot (#5500) 2024-06-10 09:04:28 +08:00
Lucas.Xu 38d2bd7ee8 fix: board shortcuts conflict with textfield (#5487)
* fix: board shortcuts conflict with text field

* fix: typo

* fix: favorite icon issue in dark mode
2024-06-10 09:04:01 +08:00
Nathan.fooo bb3e9d5bd8 feat: Stream chat message (#5498)
* chore: stream message

* chore: stream message

* chore: fix streaming

* chore: fix clippy
2024-06-09 14:02:32 +08:00
Kilu.He 94060a0a99 fix: generate default document data from document id (#5430)
* fix: generate default document data from document id

* fix: update version
2024-06-09 13:42:09 +08:00
Kilu.He 86696b271e chore: improvements to caching strategies (#5488) 2024-06-07 16:37:07 +08:00
Tianyu Yuan 76fde00cc4 chore: update zh-CN translations (#5484)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-06-07 11:48:23 +08:00
Kilu.He b794f3894e chore: improve coverage of cypress (#5483) 2024-06-06 19:47:14 +08:00
Kilu.He 3b72f90ca5 feat: improve test coverage (#5481) 2024-06-06 17:48:58 +08:00
2610 changed files with 82670 additions and 29481 deletions

No files matched your search

-28
View File
@@ -1,28 +0,0 @@
name: Build Bot
on:
issue_comment:
types: [created]
jobs:
dispatch_slash_command:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
# get build name from pubspec.yaml
- name: Get build version
working-directory: frontend/appflowy_flutter
id: get_build_name
run: |
echo "fetching version from pubspec.yaml..."
echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT
- uses: peter-evans/slash-command-dispatch@v4
with:
token: ${{ secrets.PAT }}
commands: build
static-args: |
ref=refs/pull/${{ github.event.issue.number }}/head
build_name=${{ steps.get_build_name.outputs.build_name }}
-72
View File
@@ -1,72 +0,0 @@
name: Deploy Web (Test)
on:
push:
branches:
- build/test
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:
deploy:
runs-on: ubuntu-latest
env:
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
ENV_FILE: test.env
steps:
- uses: actions/checkout@v4
- 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: copy env file
working-directory: frontend/appflowy_web_app
run: |
cp ${{ env.ENV_FILE }} .env
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build
working-directory: frontend/appflowy_web_app
run: |
pnpm run build
- name: generate SSL certificate
run: |
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
- name: Deploy to EC2
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
ARGS: "-rlgoDzvc -i"
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
REMOTE_HOST: ${{ env.REMOTE_HOST }}
REMOTE_USER: ${{ env.REMOTE_USER }}
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
SCRIPT_AFTER: |
docker build -t appflowy-web-app .
docker rm -f appflowy-web-app || true
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
+1 -1
View File
@@ -13,7 +13,7 @@ on:
- release/*
paths:
- frontend/**
types: [opened, synchronize, reopened, unlocked, ready_for_review]
types: [ opened, synchronize, reopened, unlocked, ready_for_review ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+7 -4
View File
@@ -248,10 +248,13 @@ jobs:
env:
BACKEND_VERSION: 0.3.24-amd64
run: |
docker compose down -v --remove-orphans
docker compose pull
docker compose up -d
sleep 10
if [ "$(docker ps --filter name=appflowy-cloud -q)" == "" ]; then
docker compose pull
docker compose up -d
sleep 10
else
echo "Docker container 'appflowy-cloud' is already running."
fi
- name: Checkout source code
uses: actions/checkout@v4
+25 -11
View File
@@ -28,20 +28,35 @@ concurrency:
cancel-in-progress: true
jobs:
build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
os: [ macos-14 ]
runs-on: ${{ matrix.os }}
build-self-hosted:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Build AppFlowy
working-directory: frontend
run: |
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
cargo make --profile development-ios-arm64-sim code_generation
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
model: 'iPhone 15'
shutdown_after_job: false
build-macos:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
@@ -49,8 +64,7 @@ jobs:
override: true
profile: minimal
- name: Install flutter
id: flutter
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
@@ -59,7 +73,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.os }}
prefix-key: macos-latest
workspaces: |
frontend/rust-lib
+20 -2
View File
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
job:
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" }
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -233,7 +233,7 @@ jobs:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-11,
os: macos-latest,
extra-build-args: "",
}
steps:
@@ -479,6 +479,24 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max
notify-failure:
runs-on: ubuntu-latest
needs:
- build-for-macOS-x86_64
- build-for-windows
- build-for-linux
if: failure()
steps:
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
🔴🔴🔴Workflow ${{ github.workflow }} in repository ${{ github.repository }} was failed 🔴🔴🔴.
fields: repo,message,author,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
if: always()
notify-discord:
runs-on: ubuntu-latest
needs:
+86 -52
View File
@@ -8,6 +8,7 @@ on:
- "release/*"
paths:
- "frontend/rust-lib/**"
- ".github/workflows/rust_ci.yaml"
pull_request:
branches:
@@ -22,70 +23,25 @@ env:
RUST_TOOLCHAIN: "1.77.2"
jobs:
test-on-ubuntu:
runs-on: ubuntu-latest
self-hosted-job:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
steps:
# - name: Maximize build space
# uses: easimon/maximize-build-space@master
# with:
# root-reserve-mb: 2048
# swap-size-mb: 1024
# remove-dotnet: 'true'
# # the following step is required to avoid running out of space
# - 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
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt, clippy
profile: minimal
- name: Install prerequisites
working-directory: frontend
run: |
cargo install --force cargo-make
cargo install --force duckscript_cli
- uses: Swatinem/rust-cache@v2
with:
prefix-key: "ubuntu-latest"
workspaces: |
frontend/rust-lib
- name: Checkout appflowy cloud code
- name: Checkout Appflowy Cloud
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
- name: Prepare appflowy cloud env
- name: Prepare Appflowy Cloud env
working-directory: AppFlowy-Cloud
run: |
# log level
cp deploy.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
env:
BACKEND_VERSION: 0.3.24-amd64
run: |
docker pull appflowyinc/appflowy_cloud:latest
docker compose up -d
sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- name: Run rust-lib tests
working-directory: frontend/rust-lib
@@ -106,6 +62,84 @@ jobs:
run: cargo clippy --all-targets -- -D warnings
working-directory: frontend/rust-lib
ubuntu-job:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- 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
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt, clippy
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ runner.os }}
cache-on-failure: true
workspaces: |
frontend/rust-lib
- name: Checkout appflowy cloud code
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
run: |
cp deploy.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
run: |
if [ "$(docker ps --filter name=appflowy-cloud -q)" == "" ]; then
docker compose pull
docker compose up -d
sleep 10
else
echo "Docker container 'appflowy-cloud' is already running."
fi
- name: Run rust-lib tests
working-directory: frontend/rust-lib
env:
RUST_LOG: info
RUST_BACKTRACE: 1
af_cloud_test_base_url: http://localhost
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
run: |
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
working-directory: frontend/rust-lib/
- name: clippy rust-lib
run: cargo clippy --all-targets -- -D warnings
working-directory: frontend/rust-lib
- name: "Debug: show Appflowy-Cloud container logs"
if: failure()
working-directory: AppFlowy-Cloud
run: |
docker compose logs appflowy_cloud
- name: Clean up Docker images
run: |
docker image prune -af
+39 -28
View File
@@ -1,4 +1,5 @@
name: Tauri-CI
on:
pull_request:
paths:
@@ -11,28 +12,47 @@ env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
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:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
tauri-build-self-hosted:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
runs-on: ${{ matrix.platform }}
env:
CI: true
steps:
- uses: actions/checkout@v4
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
mkdir dist
pnpm install
cd src-tauri && cargo build
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
- 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"
tauri-build-ubuntu:
if: github.event.pull_request.head.repo.full_name != github.repository
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
@@ -61,36 +81,27 @@ jobs:
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_web_app/src-tauri -> target"
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
- 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
- name: install cargo-make
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: install tauri deps tools
working-directory: frontend
run: |
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
shell: bash
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
-83
View File
@@ -1,83 +0,0 @@
name: WEB-CI
on:
workflow_dispatch:
inputs:
build:
description: 'Build the web app'
required: true
default: 'true'
env:
CARGO_TERM_COLOR: always
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
CARGO_MAKE_VERSION: "0.36.6"
jobs:
web-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Rust Dependencies
uses: Swatinem/rust-cache@v2
with:
key: rust-dependencies-${{ runner.os }}
workspaces: |
frontend/rust-lib
frontend/appflowy_web/appflowy_wasm
# TODO: Can combine caching deps and node_modules in one
# See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob
- name: Cache Node.js dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}
- name: Cache node_modules
uses: actions/cache@v4
with:
path: frontend/appflowy_web/node_modules
key: node-modules-${{ runner.os }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Install wasm-pack
run: cargo install wasm-pack
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: install dependencies
if: matrix.platform == 'ubuntu-latest'
working-directory: frontend
run: |
sudo apt-get update
npm install -g pnpm@${{ env.PNPM_VERSION }}
cargo make install_web_protobuf
- name: Build
working-directory: frontend/appflowy_web
run: |
pnpm install
pnpm run build_release_wasm
+11 -5
View File
@@ -6,6 +6,7 @@ on:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
@@ -52,8 +53,13 @@ jobs:
run: |
pnpm run test:unit
- name: Generate and post coverage summary
working-directory: frontend/appflowy_web_app
run: |
pnpm run merge-coverage
- 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
+68
View File
@@ -1,4 +1,72 @@
# Release Notes
## Version 0.6.7 - 13/08/2024
### New Features
- Redesigned the icon picker design on Desktop.
- Redesigned the notification page on Mobile.
### Bug Fixes
- Enhance the toolbar tooltip functionality on Desktop.
- Enhance the slash menu user experience on Desktop.
- Fixed the issue where list style overrides occurred during text pasting.
- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.
## Version 0.6.6 - 30/07/2024
### New Features
- Upgrade your workspace to a premium plan to unlock more features and storage.
- Image galleries and drag-and-drop image support in documents.
### Bug Fixes
- Fix minor UI issues on Desktop and Mobile.
## Version 0.6.5 - 24/07/2024
### New Features
- Publish a Database to the Web
## Version 0.6.4 - 16/07/2024
### New Features
- Enhanced the message style on the AI chat page.
- Added the ability to choose cursor color and selection color from a palette in settings page.
### Bug Fixes
- Optimized the performance for loading recent pages.
- Fixed an issue where the cursor would jump randomly when typing in the document title on mobile.
## Version 0.6.3 - 08/07/2024
### New Features
- Publish a Document to the Web
## Version 0.6.2 - 01/07/2024
### New Features
- Added support for duplicating spaces.
- Added support for moving pages across spaces.
- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`.
- Improved shortcuts settings UI.
### Bug Fixes
- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards.
- Fixed unable to paste nested lists in existing lists.
## Version 0.6.1 - 22/06/2024
### New Features
- Introduced the "Space" feature to help you organize your pages more efficiently.
### Bug Fixes
- Resolved shortcut conflicts on the board page.
- Resolved an issue where underscores could cause the editor to freeze.
## Version 0.6.0 - 19/06/2024
### New Features
- Introduced the "Space" feature to help you organize your pages more efficiently.
### Bug Fixes
- Resolved shortcut conflicts on the board page.
- Resolved an issue where underscores could cause the editor to freeze.
## Version 0.5.9 - 06/06/2024
### New Features
- Revamped the sidebar for both Desktop and Mobile.
- Added support for embedding videos in documents.
- Introduced a hotkey (Cmd/Ctrl + 0) to reset the app scale.
- Supported searching the workspace by page title.
### Bug Fixes
- Fixed the issue preventing the use of Backspace to delete words in Kanban boards.
## Version 0.5.8 - 05/20/2024
### New Features
- Improvement to the Callout block to insert new lines
+1 -2
View File
@@ -13,7 +13,6 @@
"type": "dart",
"env": {
"RUST_LOG": "debug",
"RUST_BACKTRACE": "1"
},
// uncomment the following line to testing performance.
// "flutterMode": "profile",
@@ -138,4 +137,4 @@
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
}
}
+1 -2
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.5.9"
APPFLOWY_VERSION = "0.6.7"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -50,7 +50,6 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
# Test default config
TEST_CRATE_TYPE = "cdylib"
@@ -52,8 +52,8 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 23
targetSdkVersion 33
minSdkVersion 24
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -0,0 +1 @@
{ "artificial_intelligence": [ { "name": "ai-chip-spark", "keywords": [ "chip", "processor", "artificial", "intelligence", "ai" ], "content": "<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<g clip-path=\"url(#clip0_1068_189544)\">\n<path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.71468 0.0400391C9.1289 0.0400391 9.46468 0.375825 9.46468 0.790039V1.97754H10.4999C11.3283 1.97754 11.9999 2.64911 11.9999 3.47754V4.51367H13.2186C13.6328 4.51367 13.9686 4.84946 13.9686 5.26367C13.9686 5.67789 13.6328 6.01367 13.2186 6.01367H11.9999V7.94324H13.2186C13.6328 7.94324 13.9686 8.27902 13.9686 8.69324C13.9686 9.10745 13.6328 9.44324 13.2186 9.44324H11.9999V10.4775C11.9999 11.306 11.3283 11.9775 10.4999 11.9775H9.46468V13.2099C9.46468 13.6241 9.1289 13.9599 8.71468 13.9599C8.30047 13.9599 7.96468 13.6241 7.96468 13.2099V11.9775H6.03524V13.2099C6.03524 13.6241 5.69945 13.9599 5.28524 13.9599C4.87103 13.9599 4.53524 13.6241 4.53524 13.2099V11.9775H3.49984C2.67141 11.9775 1.99984 11.306 1.99984 10.4775V9.44324H0.783203C0.368989 9.44324 0.0332031 9.10745 0.0332031 8.69324C0.0332031 8.27902 0.368989 7.94324 0.783203 7.94324H1.99984V6.01367H0.783203C0.368989 6.01367 0.0332031 5.67789 0.0332031 5.26367C0.0332031 4.84946 0.368989 4.51367 0.783203 4.51367H1.99984V3.47754C1.99984 2.64911 2.67141 1.97754 3.49984 1.97754H4.53524V0.790039C4.53524 0.375825 4.87103 0.0400391 5.28524 0.0400391C5.69945 0.0400391 6.03524 0.375825 6.03524 0.790039V1.97754H7.96468V0.790039C7.96468 0.375825 8.30047 0.0400391 8.71468 0.0400391ZM6.24357 4.5682C6.41951 3.76446 7.56392 3.75946 7.74688 4.56163L7.75499 4.59732C7.76038 4.62113 7.76543 4.64342 7.7708 4.66628C7.96264 5.48335 8.62389 6.10768 9.45164 6.25169C10.2908 6.39768 10.2908 7.60232 9.45164 7.74831C8.61948 7.89308 7.95559 8.52332 7.76776 9.34682L7.74688 9.43837C7.56392 10.2405 6.41951 10.2355 6.24357 9.4318L6.22638 9.35325C6.04547 8.52684 5.38257 7.89201 4.54911 7.74701C3.71142 7.60128 3.71141 6.39872 4.54911 6.25299C5.37967 6.10849 6.04086 5.47757 6.22447 4.65539L6.23707 4.598L6.24357 4.5682Z\" fill=\"black\"/>\n</g>\n<defs>\n<clipPath id=\"clip0_1068_189544\">\n<rect width=\"14\" height=\"14\" fill=\"white\"/>\n</clipPath>\n</defs>\n</svg>\n" }, { "name": "ai-cloud-spark", "keywords": [ "cloud", "internet", "server", "network", "artificial", "intelligence", "ai" ], "content": "<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <g clip-path=\"url(#clip0_1068_189529)\">\n <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n d=\"M7.88381 0.165291C6.89196 0.0450664 5.88886 0.268505 5.04181 0.798345C4.25349 1.29145 3.64467 2.02219 3.30135 2.88247C2.87262 2.92977 2.45508 3.05187 2.06781 3.24362C1.63324 3.45878 1.24531 3.75744 0.92616 4.12253C0.281611 4.85986 -0.0436335 5.82304 0.0219743 6.80017C0.087582 7.77731 0.538668 8.68836 1.276 9.33291C1.52925 9.55429 1.80913 9.738 2.10708 9.88114C2.23853 9.09496 2.79956 8.38069 3.79016 8.20836C4.35848 8.10949 4.81104 7.67811 4.93736 7.11585L4.96023 7.01146C5.43591 4.83845 8.53 4.82492 9.02466 6.9937L9.03475 7.03814C9.04166 7.06865 9.047 7.09221 9.05244 7.11538C9.18449 7.6778 9.63973 8.10763 10.2096 8.20676C11.2609 8.38966 11.8287 9.18251 11.9131 10.0243C12.2792 9.884 12.6197 9.67707 12.9162 9.41068C13.5042 8.88231 13.878 8.15668 13.9669 7.37119C14.0558 6.58569 13.8536 5.79486 13.3985 5.14845C12.9811 4.55545 12.3775 4.12175 11.6866 3.91417C11.5613 2.99088 11.1436 2.12994 10.4923 1.45901C9.79633 0.742138 8.87567 0.285516 7.88381 0.165291ZM6.18132 7.27876C6.37146 6.41016 7.60823 6.40476 7.80595 7.27166L7.81577 7.3149C7.8225 7.34462 7.82879 7.37239 7.83553 7.4011C8.08204 8.451 8.93171 9.25323 9.99531 9.43826C10.9004 9.59573 10.9004 10.8951 9.99531 11.0525C8.92604 11.2386 8.07299 12.0484 7.83164 13.1065L7.80595 13.2191C7.60823 14.086 6.37146 14.0806 6.18132 13.212L6.16017 13.1154C5.92765 12.0532 5.07564 11.2373 4.0044 11.0509C3.10108 10.8938 3.10107 9.59701 4.0044 9.43986C5.07191 9.25414 5.92172 8.44323 6.15771 7.38648L6.17343 7.31494L6.18132 7.27876Z\"\n fill=\"black\"/>\n </g>\n <defs>\n <clipPath id=\"clip0_1068_189529\">\n <rect width=\"14\" height=\"14\" fill=\"white\"/>\n </clipPath>\n </defs>\n</svg>\n" }, { "name": "ai-edit-spark", "keywords": [ "change", "edit", "modify", "pencil", "write", "writing", "artificial", "intelligence", "ai" ], "content": "<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<g clip-path=\"url(#clip0_1068_189535)\">\n<path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.85637 0.654372C3.04651 -0.214226 4.28328 -0.219632 4.481 0.647271L4.49082 0.690505C4.49756 0.720231 4.50384 0.747995 4.51058 0.776709C4.75709 1.82661 5.60676 2.62884 6.67036 2.81387C7.57548 2.97134 7.57548 4.27068 6.67036 4.42815C5.60109 4.61417 4.74804 5.42398 4.50669 6.48214L4.481 6.59475C4.28328 7.46166 3.04651Line truncated
@@ -42,7 +42,7 @@ void main() {
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// reanme the name of the anon user
// rename the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
await tester.pumpAndSettle();
@@ -47,11 +47,11 @@ void main() {
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeText(LocaleKeys.button_confirm.tr());
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpAndSettle(const Duration(seconds: 5));
tester.expectToSeeGoogleLoginButton();
});
@@ -1,7 +1,6 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
// import 'document_sync_test.dart' as document_sync_test;
import 'user_setting_sync_test.dart' as user_sync_test;
import 'workspace/change_name_and_icon_test.dart'
as change_workspace_name_and_icon_test;
@@ -10,13 +9,8 @@ import 'workspace/collaborative_workspace_test.dart'
Future<void> main() async {
preset_af_cloud_env_test.main();
appflowy_cloud_auth_test.main();
// document_sync_test.main();
user_sync_test.main();
anon_user_continue_test.main();
// workspace
@@ -46,7 +46,6 @@ void main() {
await tester.ime.insertText(inputContent);
expect(find.text(inputContent, findRichText: true), findsOneWidget);
// TODO(nathan): remove the await
// 6 seconds for data sync
await tester.waitForSeconds(6);
@@ -50,26 +50,28 @@ void main() {
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpAndSettle(const Duration(seconds: 6));
});
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
});
});
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
});
}
@@ -32,69 +32,69 @@ import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final email = '${uuid()}@appflowy.io';
// final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
// if (!FeatureFlag.collaborativeWorkspace.isOn) {
// return;
// }
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloudSelfHost,
// email: email,
// );
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
const name = 'AppFlowy.IO';
// the workspace will be opened after created
await tester.createCollaborativeWorkspace(name);
// const name = 'AppFlowy.IO';
// // the workspace will be opened after created
// await tester.createCollaborativeWorkspace(name);
final loading = find.byType(Loading);
await tester.pumpUntilNotFound(loading);
// final loading = find.byType(Loading);
// await tester.pumpUntilNotFound(loading);
Finder success;
// Finder success;
final Finder items = find.byType(WorkspaceMenuItem);
// final Finder items = find.byType(WorkspaceMenuItem);
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
await tester.pumpUntilFound(items);
// // delete the newly created workspace
// await tester.openCollaborativeWorkspaceMenu();
// await tester.pumpUntilFound(items);
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
name,
);
// expect(items, findsNWidgets(2));
// expect(
// tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
// name,
// );
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget(
secondWorkspace,
onHover: () async {
// click the more button
final moreButton = find.byType(WorkspaceMoreActionList);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
// click the delete button
final deleteButton = find.text(LocaleKeys.button_delete.tr());
expect(deleteButton, findsOneWidget);
await tester.tapButton(deleteButton);
// see the delete confirm dialog
final confirm =
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
expect(confirm, findsOneWidget);
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr());
await tester.pumpUntilFound(success);
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
},
);
// final secondWorkspace = find.byType(WorkspaceMenuItem).last;
// await tester.hoverOnWidget(
// secondWorkspace,
// onHover: () async {
// // click the more button
// final moreButton = find.byType(WorkspaceMoreActionList);
// expect(moreButton, findsOneWidget);
// await tester.tapButton(moreButton);
// // click the delete button
// final deleteButton = find.text(LocaleKeys.button_delete.tr());
// expect(deleteButton, findsOneWidget);
// await tester.tapButton(deleteButton);
// // see the delete confirm dialog
// final confirm =
// find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
// expect(confirm, findsOneWidget);
// await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// // delete success
// success = find.text(LocaleKeys.workspace_createSuccess.tr());
// await tester.pumpUntilFound(success);
// expect(success, findsOneWidget);
// await tester.pumpUntilNotFound(success);
// },
// );
});
});
}
@@ -29,6 +29,7 @@ void main() {
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
await tester.tapOKButton();
expect(find.text(name), findsNothing);
});
@@ -41,15 +41,16 @@ void main() {
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid();
await tester.hoverOnFirstRowOfGrid(() async {
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.pumpAndSettle();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle();
// Open the row menu and then click the delete
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
// 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2);
await tester.pumpAndSettle();
// 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2);
});
});
testWidgets('check number of row indicator in the initial grid',
@@ -1,10 +1,10 @@
import 'dart:io';
import 'package:flutter/services.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_editor/appflowy_editor.dart';
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';
@@ -25,6 +25,7 @@ void main() {
const lines = 3;
final text = List.generate(lines, (index) => 'line $index').join('\n');
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
ClipboardService.mockSetData(ClipboardServiceData(plainText: text));
await insertCodeBlockInDocument(tester);
@@ -51,7 +52,9 @@ Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
// open the actions menu and insert the codeBlock
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_selectionMenu_codeBlock.tr(),
LocaleKeys.document_slashMenu_name_code.tr(),
offset: 150,
);
// wait for the codeBlock to be inserted
await tester.pumpAndSettle();
}
@@ -116,6 +116,91 @@ void main() {
]);
});
});
testWidgets('paste bulleted list in numbered list', (tester) async {
const inAppJson =
'{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}';
await tester.pasteContent(
inAppJson: inAppJson,
beforeTest: (editorState) async {
final transaction = editorState.transaction;
// Insert two numbered list nodes
// 1. Parent One
// 2.
transaction.insertNodes(
[0],
[
Node(
type: NumberedListBlockKeys.type,
attributes: {
'delta': [
{"insert": "One"},
],
},
),
Node(
type: NumberedListBlockKeys.type,
attributes: {'delta': []},
),
],
);
// Set the selection to the second numbered list node (which has empty delta)
transaction.afterSelection = Selection.collapsed(Position(path: [1]));
await editorState.apply(transaction);
await tester.pumpAndSettle();
},
(editorState) {
final secondNode = editorState.getNodeAtPath([1]);
expect(secondNode?.delta?.toPlainText(), 'Hello');
expect(secondNode?.children.length, 1);
final childNode = secondNode?.children.first;
expect(childNode?.delta?.toPlainText(), 'World');
expect(childNode?.type, BulletedListBlockKeys.type);
},
);
});
});
testWidgets('paste text on part of bullet list', (tester) async {
const plainText = 'test';
await tester.pasteContent(
plainText: plainText,
beforeTest: (editorState) async {
final transaction = editorState.transaction;
transaction.insertNodes(
[0],
[
Node(
type: BulletedListBlockKeys.type,
attributes: {
'delta': [
{"insert": "bullet list"},
],
},
),
],
);
// Set the selection to the second numbered list node (which has empty delta)
transaction.afterSelection = Selection(
start: Position(path: [0], offset: 7),
end: Position(path: [0], offset: 11),
);
await editorState.apply(transaction);
await tester.pumpAndSettle();
},
(editorState) {
final node = editorState.getNodeAtPath([0]);
expect(node?.delta?.toPlainText(), 'bullet test');
expect(node?.type, BulletedListBlockKeys.type);
},
);
});
testWidgets('paste image(png) from memory', (tester) async {
@@ -198,10 +283,6 @@ void main() {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(
node.attributes[ImageBlockKeys.url],
'https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png',
);
},
);
},
@@ -210,42 +291,33 @@ void main() {
testWidgets('paste the html content contains section', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
},
);
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
});
});
testWidgets('paste the html from google translation', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 8);
},
);
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 8);
});
});
testWidgets(
'auto convert url to link preview block',
(widgetTester) async {
(tester) async {
const url = 'https://appflowy.io';
await widgetTester.pasteContent(
plainText: url,
(editorState) {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
},
);
await tester.pasteContent(plainText: url, (editorState) {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
},
);
}
@@ -256,6 +328,7 @@ extension on WidgetTester {
Future<void> Function(EditorState editorState)? beforeTest,
String? plainText,
String? html,
String? inAppJson,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
@@ -271,6 +344,7 @@ extension on WidgetTester {
ClipboardServiceData(
plainText: plainText,
html: html,
inAppJson: inAppJson,
image: image,
),
);
@@ -12,10 +12,13 @@ import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
import 'document_with_multi_image_block_test.dart'
as document_with_multi_image_block_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
@@ -38,6 +41,8 @@ void startTesting() {
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
document_with_file_test.main();
}
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
@@ -7,7 +6,6 @@ import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.d
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -22,7 +20,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// validate the referenced grid is inserted
expect(
@@ -50,7 +48,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Board);
await insertLinkedDatabase(tester, ViewLayoutPB.Board);
// validate the referenced board is inserted
expect(
@@ -66,7 +64,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
await insertLinkedDatabase(tester, ViewLayoutPB.Calendar);
// validate the referenced grid is inserted
expect(
@@ -129,7 +127,7 @@ void main() {
}
/// Insert a referenced database of [layout] into the document
Future<void> insertReferenceDatabase(
Future<void> insertLinkedDatabase(
WidgetTester tester,
ViewLayoutPB layout,
) async {
@@ -150,7 +148,7 @@ Future<void> insertReferenceDatabase(
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.referencedMenuName,
layout.slashMenuLinkedName,
);
final linkToPageMenu = find.byType(InlineActionsHandler);
@@ -176,16 +174,8 @@ Future<void> createInlineDatabase(
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced view
await tester.editor.showSlashMenu();
final name = switch (layout) {
ViewLayoutPB.Grid => LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
ViewLayoutPB.Board =>
LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
ViewLayoutPB.Calendar =>
LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(),
_ => '',
};
await tester.editor.tapSlashMenuItemWithName(
name,
layout.slashMenuName,
);
await tester.pumpAndSettle();
@@ -0,0 +1,169 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/startup/startup.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 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
group('file block in document', () {
testWidgets('insert a file from local file + rename file', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(name: 'Insert file test');
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_file.tr(),
);
expect(find.byType(FileBlockComponent), findsOneWidget);
await tester.tap(find.byType(FileBlockComponent));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(FileUploadMenu), findsOneWidget);
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final filePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(filePath)..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(paths: [filePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tap(
find.text(LocaleKeys.document_plugins_file_fileUploadHint.tr()),
);
await tester.pumpAndSettle();
expect(find.byType(FileUploadMenu), findsNothing);
expect(find.byType(FileBlockComponent), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, FileBlockKeys.type);
expect(node.attributes[FileBlockKeys.url], isNotEmpty);
expect(
node.attributes[FileBlockKeys.urlType],
FileUrlType.local.toIntValue(),
);
// Check the name of the file is correctly extracted
expect(node.attributes[FileBlockKeys.name], 'sample.jpeg');
expect(find.text('sample.jpeg'), findsOneWidget);
const newName = "Renamed file";
// Hover on the widget to see the three dots to open FileBlockMenu
await tester.hoverOnWidget(
find.byType(FileBlockComponent),
onHover: () async {
await tester.tap(find.byType(FileMenuTrigger));
await tester.pumpAndSettle();
await tester.tap(
find.text(LocaleKeys.document_plugins_file_renameFile_title.tr()),
);
},
);
await tester.pumpAndSettle();
expect(find.byType(FlowyTextField), findsOneWidget);
await tester.enterText(find.byType(FlowyTextField), newName);
await tester.pump();
await tester.tap(find.text(LocaleKeys.button_save.tr()));
await tester.pumpAndSettle();
final updatedNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(updatedNode.attributes[FileBlockKeys.name], newName);
expect(find.text(newName), findsOneWidget);
// remove the temp file
file.deleteSync();
});
testWidgets('insert a file from network', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(name: 'Insert file test');
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_file.tr(),
);
expect(find.byType(FileBlockComponent), findsOneWidget);
await tester.tap(find.byType(FileBlockComponent));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(FileUploadMenu), findsOneWidget);
// Navigate to integrate link tab
await tester.tapButtonWithName(
LocaleKeys.document_plugins_file_networkTab.tr(),
);
await tester.pumpAndSettle();
const url =
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
await tester.enterText(
find.descendant(
of: find.byType(FileUploadMenu),
matching: find.byType(FlowyTextField),
),
url,
);
await tester.tapButton(
find.descendant(
of: find.byType(FileUploadMenu),
matching: find.text(
LocaleKeys.document_plugins_file_networkAction.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
expect(find.byType(FileUploadMenu), findsNothing);
expect(find.byType(FileBlockComponent), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, FileBlockKeys.type);
expect(node.attributes[FileBlockKeys.url], isNotEmpty);
expect(
node.attributes[FileBlockKeys.urlType],
FileUrlType.network.toIntValue(),
);
// Check the name is correctly extracted from the url
expect(
node.attributes[FileBlockKeys.name],
'photo-1469474968028-56623f02e42e',
);
expect(find.text('photo-1469474968028-56623f02e42e'), findsOneWidget);
});
});
}
@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/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/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.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;
@@ -36,13 +36,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
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('Image');
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -84,13 +86,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
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('Image');
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -137,13 +141,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
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('Image');
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -161,5 +167,69 @@ void main() {
expect(find.byType(UnsplashImageWidget), findsOneWidget);
});
});
testWidgets('insert two images from local file at once', (tester) 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);
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsNWidgets(2));
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(firstNode.type, ImageBlockKeys.type);
expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty);
final secondNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(secondNode.type, ImageBlockKeys.type);
expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty);
// remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
});
}
@@ -33,7 +33,7 @@ void main() {
);
// tap the inline math equation button
final inlineMathEquationButton = find.byTooltip(
final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
@@ -78,7 +78,7 @@ void main() {
);
// tap the inline math equation button
var inlineMathEquationButton = find.byTooltip(
var inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
@@ -93,11 +93,11 @@ void main() {
);
// expect to the see the inline math equation button is highlighted
inlineMathEquationButton = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
widget.tooltip ==
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
inlineMathEquationButton = find.descendant(
of: find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
),
matching: find.byType(SVGIconItemWidget),
);
expect(
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,
@@ -0,0 +1,293 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.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/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.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_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/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
setUp(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
});
group('multi image block in document', () {
testWidgets('insert images from local and use interactive viewer',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'multi image block test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
await tester.tap(find.byType(MultiImagePlaceholder));
await tester.pumpAndSettle();
expect(find.byType(UploadImageMenu), findsOneWidget);
final firstImage =
await rootBundle.load('assets/test/images/sample.jpeg');
final secondImage =
await rootBundle.load('assets/test/images/sample.gif');
final tempDirectory = await getTemporaryDirectory();
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final firstFile = File(firstImagePath)
..writeAsBytesSync(firstImage.buffer.asUint8List());
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
final secondFile = File(secondImagePath)
..writeAsBytesSync(secondImage.buffer.asUint8List());
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ImageBrowserLayout), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, MultiImageBlockKeys.type);
final data = MultiImageData.fromJson(
node.attributes[MultiImageBlockKeys.images],
);
expect(data.images.length, 2);
// Start using the interactive viewer to view the image(s)
final imageFinder = find
.byWidgetPredicate(
(w) =>
w is Image &&
w.image is FileImage &&
(w.image as FileImage).file.path.endsWith('.jpeg'),
)
.first;
await tester.tap(imageFinder);
await tester.pump(kDoubleTapMinTime);
await tester.tap(imageFinder);
await tester.pumpAndSettle();
final ivFinder = find.byType(InteractiveImageViewer);
expect(ivFinder, findsOneWidget);
// go to next image
await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s));
await tester.pumpAndSettle();
// Expect image to end with .gif
final gifImageFinder = find.byWidgetPredicate(
(w) =>
w is Image &&
w.image is FileImage &&
(w.image as FileImage).file.path.endsWith('.gif'),
);
gifImageFinder.evaluate();
expect(gifImageFinder.found.length, 2);
// go to previous image
await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s));
await tester.pumpAndSettle();
gifImageFinder.evaluate();
expect(gifImageFinder.found.length, 1);
// remove the temp files
await Future.wait([firstFile.delete(), secondFile.delete()]);
});
testWidgets('insert and delete images from network', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'multi image block test',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
await tester.tap(find.byType(MultiImagePlaceholder));
await tester.pumpAndSettle();
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
);
const url =
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
await tester.enterText(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField),
),
url,
);
await tester.pumpAndSettle();
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(ImageBrowserLayout), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, MultiImageBlockKeys.type);
final data = MultiImageData.fromJson(
node.attributes[MultiImageBlockKeys.images],
);
expect(data.images.length, 1);
final imageFinder = find
.byWidgetPredicate(
(w) => w is FlowyNetworkImage && w.url == url,
)
.first;
// Insert two images from network
for (int i = 0; i < 2; i++) {
// Hover on the image to show the image toolbar
await tester.hoverOnWidget(
imageFinder,
onHover: () async {
// Click on the add
final addFinder = find.descendant(
of: find.byType(MultiImageMenu),
matching: find.byFlowySvg(FlowySvgs.add_s),
);
expect(addFinder, findsOneWidget);
await tester.tap(addFinder);
await tester.pumpAndSettle();
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.pumpAndSettle();
await tester.tapButton(
find.descendant(
of: find.byType(EmbedImageUrlWidget),
matching: find.text(
LocaleKeys.document_imageBlock_embedLink_label.tr(),
findRichText: true,
),
),
);
await tester.pumpAndSettle();
},
);
}
await tester.pumpAndSettle();
// There should be 4 images visible now, where 2 are thumbnails
expect(find.byType(ThumbnailItem), findsNWidgets(3));
// And all three use ImageRender
expect(find.byType(ImageRender), findsNWidgets(4));
// Hover on and delete the first thumbnail image
await tester.hoverOnWidget(find.byType(ThumbnailItem).first);
final deleteFinder = find
.descendant(
of: find.byType(ThumbnailItem),
matching: find.byFlowySvg(FlowySvgs.delete_s),
)
.first;
expect(deleteFinder, findsOneWidget);
await tester.tap(deleteFinder);
await tester.pumpAndSettle();
expect(find.byType(ImageRender), findsNWidgets(3));
// Delete one from interactive viewer
await tester.tap(imageFinder);
await tester.pump(kDoubleTapMinTime);
await tester.tap(imageFinder);
await tester.pumpAndSettle();
final ivFinder = find.byType(InteractiveImageViewer);
expect(ivFinder, findsOneWidget);
await tester.tap(
find.descendant(
of: find.byType(InteractiveImageToolbar),
matching: find.byFlowySvg(FlowySvgs.delete_s),
),
);
await tester.pumpAndSettle();
expect(find.byType(InteractiveImageViewer), findsNothing);
// There should be 1 image and the thumbnail for said image still visible
expect(find.byType(ImageRender), findsNWidgets(2));
});
});
}
@@ -171,7 +171,8 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
// open the actions menu and insert the outline block
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_selectionMenu_outline.tr(),
LocaleKeys.document_slashMenu_name_outline.tr(),
offset: 100,
);
await tester.pumpAndSettle();
}
@@ -1,5 +1,5 @@
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -17,25 +17,25 @@ void main() {
await tester.openSettingsPage(SettingsPage.notifications);
await tester.pumpAndSettle();
final switchFinder = find.byType(Switch).first;
final toggleFinder = find.byType(Toggle).first;
// Defaults to enabled
Switch switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, true);
Toggle toggleWidget = tester.widget(toggleFinder);
expect(toggleWidget.value, true);
// Disable
await tester.tap(switchFinder);
await tester.tap(toggleFinder);
await tester.pumpAndSettle();
switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, false);
toggleWidget = tester.widget(toggleFinder);
expect(toggleWidget.value, false);
// Enable again
await tester.tap(switchFinder);
await tester.tap(toggleFinder);
await tester.pumpAndSettle();
switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, true);
toggleWidget = tester.widget(toggleFinder);
expect(toggleWidget.value, true);
});
});
}
@@ -0,0 +1,50 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/auth_operation.dart';
import '../../shared/base.dart';
import '../../shared/expectation.dart';
import '../../shared/settings.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings Billing', () {
testWidgets('Local auth cannot see plan+billing', (tester) async {
await tester.initializeAppFlowy();
await tester.tapSignInAsGuest();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.pumpAndSettle();
// We check that another settings page is present to ensure
// it's not a fluke
expect(
find.text(
LocaleKeys.settings_workspacePage_menuLabel.tr(),
skipOffstage: false,
),
findsOneWidget,
);
expect(
find.text(
LocaleKeys.settings_planPage_menuLabel.tr(),
skipOffstage: false,
),
findsNothing,
);
expect(
find.text(
LocaleKeys.settings_billingPage_menuLabel.tr(),
skipOffstage: false,
),
findsNothing,
);
});
});
}
@@ -1,9 +1,13 @@
import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'settings_billing_test.dart' as settings_billing_test;
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
notifications_settings_test.main();
settings_billing_test.main();
shortcuts_settings_test.main();
}
@@ -0,0 +1,90 @@
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/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
import '../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('shortcuts test', () {
testWidgets('can change and overwrite shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.shortcuts);
await tester.pumpAndSettle();
final backspaceCmd =
LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();
// Input "Delete" into the search field
await tester.enterText(find.byType(TextField), backspaceCmd);
await tester.pumpAndSettle();
await tester.hoverOnWidget(
find.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(backspaceCmd),
),
onHover: () async {
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
await tester.pumpAndSettle();
await FlowyTestKeyboard.simulateKeyDownEvent(
[
LogicalKeyboardKey.delete,
LogicalKeyboardKey.enter,
],
tester: tester,
);
await tester.pumpAndSettle();
},
);
// We expect to see conflict dialog
expect(
find.text(
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
),
findsOneWidget,
);
// Press on confirm label
await tester.tap(
find.text(
LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),
),
);
await tester.pumpAndSettle();
// We expect the first ShortcutSettingTile to have one
// [KeyBadge] with `delete` label
final first = tester.widget(find.byType(ShortcutSettingTile).first)
as ShortcutSettingTile;
expect(
first.command.command,
'delete',
);
// And the second one which is `Delete left character` to have none
// as it will have been overwritten
final second = tester.widget(find.byType(ShortcutSettingTile).at(1))
as ShortcutSettingTile;
expect(
second.command.command,
'',
);
});
});
}
@@ -8,9 +8,10 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Empty', () {
testWidgets('toggle theme mode', (tester) async {
testWidgets('empty test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.wait(1000);
});
});
}
@@ -82,11 +82,11 @@ void main() {
HeadingBlockKeys.type,
);
expect(
importedPageEditorState.getNodeAtPath([2])!.type,
importedPageEditorState.getNodeAtPath([1])!.type,
HeadingBlockKeys.type,
);
expect(
importedPageEditorState.getNodeAtPath([4])!.type,
importedPageEditorState.getNodeAtPath([2])!.type,
TableBlockKeys.type,
);
});
@@ -1,113 +0,0 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.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/mock/mock_openai_repository.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
group('integration tests for open-ai smart menu', () {
setUpAll(() async => service.setUpAll());
setUp(() async => service.setUp());
testWidgets('testing selection on open-ai smart menu replace',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 10),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 84),
),
);
});
testWidgets('testing selection on open-ai smart menu insert',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1]),
end: Position(path: [1], offset: 5),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [2]),
end: Position(path: [3]),
),
);
});
});
}
Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
await tester.initializeAppFlowy();
await mockOpenAIRepository();
await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
await tester.pumpAndSettle();
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
return tester.state(editor).widget as AppFlowyEditor;
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
);
return;
}
@@ -1,6 +1,6 @@
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -51,13 +51,13 @@ void main() {
},
);
final shareButton = find.byType(ShareActionList);
final shareButtonState =
tester.state(shareButton) as ShareActionListState;
final shareButton = find.byType(ShareButton);
final shareButtonState = tester.widget(shareButton) as ShareButton;
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.name}.md',
'${shareButtonState.view.name}.md',
),
);
@@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
await tapButton(
find.byKey(const Key('signInWithGoogleButton')),
);
}
/// Requires being on the SettingsPage.account of the SettingsDialog
@@ -26,8 +28,8 @@ extension AppFlowyAuthTest on WidgetTester {
await tapButton(find.byType(SignInOutButton));
expectToSeeText(LocaleKeys.button_confirm.tr());
await tapButtonWithName(LocaleKeys.button_confirm.tr());
expectToSeeText(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr());
}
Future<void> tapSignInAsGuest() async {
@@ -44,6 +46,12 @@ extension AppFlowyAuthTest on WidgetTester {
assert(isSwitched == value);
}
void assertToggleValue(Finder finder, bool value) {
final Toggle switchWidget = widget(finder);
final isSwitched = switchWidget.value;
assert(isSwitched == value);
}
void assertEnableEncryptSwitchValue(bool value) {
assertSwitchValue(
find.descendant(
@@ -65,10 +73,10 @@ extension AppFlowyAuthTest on WidgetTester {
}
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
assertSwitchValue(
assertToggleValue(
find.descendant(
of: find.byType(AppFlowyCloudEnableSync),
matching: find.byWidgetPredicate((widget) => widget is Switch),
matching: find.byWidgetPredicate((widget) => widget is Toggle),
),
value,
);
@@ -86,7 +94,7 @@ extension AppFlowyAuthTest on WidgetTester {
Future<void> toggleEnableSync(Type syncButton) async {
final finder = find.descendant(
of: find.byType(syncButton),
matching: find.byWidgetPredicate((widget) => widget is Switch),
matching: find.byWidgetPredicate((widget) => widget is Toggle),
);
await tapButton(finder);
@@ -1,10 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
@@ -17,6 +13,8 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -37,8 +35,6 @@ extension AppFlowyTestBase on WidgetTester {
AuthenticatorType? cloudType,
String? email,
}) async {
VideoBlockKit.ensureInitialized();
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
// Set the window size
await binding.setSurfaceSize(windowSize);
@@ -234,6 +230,16 @@ extension AppFlowyFinderTestBase on CommonFinders {
(widget) => widget is FlowyText && widget.text == text,
);
}
Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) {
return byWidgetPredicate(
(widget) =>
widget is FlowyTooltip &&
widget.richMessage != null &&
widget.richMessage!.toPlainText().contains(richMessage),
skipOffstage: skipOffstage,
);
}
}
Future<void> useTestSupabaseCloud() async {
@@ -6,7 +6,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
@@ -22,6 +22,7 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
@@ -257,7 +258,7 @@ extension CommonOperations on WidgetTester {
/// Tap the share button above the document page.
Future<void> tapShareButton() async {
final shareButton = find.byWidgetPredicate(
(widget) => widget is DocumentShareButton,
(widget) => widget is ShareButton,
);
await tapButton(shareButton);
}
@@ -619,6 +620,18 @@ extension SettingsFinder on CommonFinders {
matching: find.byType(Scrollable),
)
.first;
Finder findSettingsMenuScrollable() => find
.descendant(
of: find
.descendant(
of: find.byType(SettingsMenu),
matching: find.byType(SingleChildScrollView),
)
.first,
matching: find.byType(Scrollable),
)
.first;
}
extension ViewLayoutPBTest on ViewLayoutPB {
@@ -649,4 +662,34 @@ extension ViewLayoutPBTest on ViewLayoutPB {
throw UnsupportedError('Unsupported layout: $this');
}
}
String get slashMenuName {
switch (this) {
case ViewLayoutPB.Grid:
return LocaleKeys.document_slashMenu_name_grid.tr();
case ViewLayoutPB.Board:
return LocaleKeys.document_slashMenu_name_kanban.tr();
case ViewLayoutPB.Document:
return LocaleKeys.document_slashMenu_name_doc.tr();
case ViewLayoutPB.Calendar:
return LocaleKeys.document_slashMenu_name_calendar.tr();
default:
throw UnsupportedError('Unsupported layout: $this');
}
}
String get slashMenuLinkedName {
switch (this) {
case ViewLayoutPB.Grid:
return LocaleKeys.document_slashMenu_name_linkedGrid.tr();
case ViewLayoutPB.Board:
return LocaleKeys.document_slashMenu_name_linkedKanban.tr();
case ViewLayoutPB.Document:
return LocaleKeys.document_slashMenu_name_linkedDoc.tr();
case ViewLayoutPB.Calendar:
return LocaleKeys.document_slashMenu_name_linkedCalendar.tr();
default:
throw UnsupportedError('Unsupported layout: $this');
}
}
}
@@ -130,12 +130,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
await openPage('v020', layout: ViewLayoutPB.Grid);
}
Future<void> hoverOnFirstRowOfGrid() async {
Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {
final findRow = find.byType(GridRow);
expect(findRow, findsWidgets);
final firstRow = findRow.first;
await hoverOnWidget(firstRow);
await hoverOnWidget(firstRow, onHover: onHover);
}
Future<void> editCell({
@@ -876,11 +876,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapRowMenuButtonInGrid() async {
expect(find.byType(RowMenuButton), findsOneWidget);
await tapButton(find.byType(RowMenuButton));
}
/// Should call [tapRowMenuButtonInGrid] first.
Future<void> tapDeleteOnRowMenu() async {
expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget);
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
}
@@ -3,15 +3,15 @@ import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -84,7 +84,7 @@ class EditorOperations {
final Finder button = !isInPicker
? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())
: find.descendant(
of: find.byType(FlowyIconPicker),
of: find.byType(FlowyIconEmojiPicker),
matching: find.text(LocaleKeys.button_remove.tr()),
);
await tester.tapButton(button);
@@ -170,8 +170,26 @@ class EditorOperations {
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
Future<void> tapSlashMenuItemWithName(String name) async {
Future<void> tapSlashMenuItemWithName(
String name, {
double offset = 200,
}) async {
final slashMenu = find
.ancestor(
of: find.byType(SelectionMenuItemWidget),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable,
),
)
.first;
final slashMenuItem = find.text(name, findRichText: true);
await tester.scrollUntilVisible(
slashMenuItem,
offset,
scrollable: slashMenu,
duration: const Duration(milliseconds: 250),
);
assert(slashMenuItem.hasFound);
await tester.tapButton(slashMenuItem);
}
@@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
@@ -18,10 +18,17 @@ import 'common_operations.dart';
extension AppFlowySettings on WidgetTester {
/// Open settings page
Future<void> openSettings() async {
final settingsDialog = find.byType(SettingsDialog);
// tap empty area to close the settings page
while (settingsDialog.evaluate().isNotEmpty) {
await tapAt(Offset.zero);
await pumpAndSettle();
}
final settingsButton = find.byType(UserSettingButton);
expect(settingsButton, findsOneWidget);
await tapButton(settingsButton);
final settingsDialog = find.byType(SettingsDialog);
expect(settingsDialog, findsOneWidget);
return;
}
@@ -31,6 +38,14 @@ extension AppFlowySettings on WidgetTester {
final button = find.byWidgetPredicate(
(widget) => widget is SettingsMenuElement && widget.page == page,
);
await scrollUntilVisible(
button,
0,
scrollable: find.findSettingsMenuScrollable(),
);
await pump();
expect(button, findsOneWidget);
await tapButton(button);
return;
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.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_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
@@ -58,7 +58,7 @@ extension AppFlowyWorkspace on WidgetTester {
);
expect(iconButton, findsOneWidget);
await tapButton(iconButton);
final iconPicker = find.byType(FlowyIconPicker);
final iconPicker = find.byType(FlowyIconEmojiPicker);
expect(iconPicker, findsOneWidget);
await tapButton(find.findTextInFlowyText(icon));
}
+7 -37
View File
@@ -58,12 +58,6 @@ PODS:
- Flutter
- keyboard_height_plugin (0.0.1):
- Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -71,9 +65,9 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- screen_brightness_ios (0.1.0):
- printing (1.0.0):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
@@ -91,10 +85,6 @@ PODS:
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -110,20 +100,15 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- 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`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- printing (from `.symlinks/plugins/printing/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`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
@@ -161,20 +146,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/irondash_engine_context/ios"
keyboard_height_plugin:
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
printing:
:path: ".symlinks/plugins/printing/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -185,10 +164,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
@@ -200,20 +175,17 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
@@ -222,8 +194,6 @@ SPEC CHECKSUMS:
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -18,6 +18,13 @@ class KVKeys {
/// {'dx': 10.0, 'dy': 10.0}
static const String windowPosition = 'windowPosition';
/// The key for saving the window status
///
/// The value is a json string with the following format:
/// { 'windowMaximized': true }
///
static const String windowMaximized = 'windowMaximized';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
@@ -81,14 +88,24 @@ class KVKeys {
/// The value is a double string.
static const String scaleFactor = 'scaleFactor';
/// The key for saving the last opened space
/// The key for saving the last opened tab (favorite, recent, space etc.)
///
/// The value is a int string.
static const String lastOpenedSpace = 'lastOpenedSpace';
/// The key for saving the space order
/// The key for saving the space tab order
///
/// The value is a json string with the following format:
/// [0, 1, 2]
static const String spaceOrder = 'spaceOrder';
/// The key for saving the last opened space id (space A, space B)
///
/// The value is a string.
static const String lastOpenedSpaceId = 'lastOpenedSpaceId';
/// The key for saving the upgrade space tag
///
/// The value is a boolean string
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
}
@@ -3,10 +3,9 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/window_title_bar.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -103,34 +102,38 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
return const SizedBox.shrink();
}
final color = Theme.of(context).isLightMode ? Colors.white : Colors.black;
final textSpan = TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.sideBar_openSidebar.tr()}\n',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color),
style: context.tooltipTextStyle(),
),
TextSpan(
text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
style: context
.tooltipTextStyle()
?.copyWith(color: Theme.of(context).hintColor),
),
],
);
return FlowyTooltip(
richMessage: textSpan,
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => context
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.all(4.0),
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
? const FlowySvg(FlowySvgs.show_menu_s)
: const FlowySvg(FlowySvgs.hide_menu_m),
child: FlowyHover(
child: Container(
width: 24,
padding: const EdgeInsets.all(4),
child: const RotatedBox(
quarterTurns: 2,
child: FlowySvg(FlowySvgs.hide_menu_s),
),
),
),
),
);
}
@@ -1,9 +1,10 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;
+7
View File
@@ -29,4 +29,11 @@ abstract class Env {
defaultValue: '',
)
static const String afCloudUrl = _Env.afCloudUrl;
@EnviedField(
obfuscate: false,
varName: 'INTERNAL_BUILD',
defaultValue: '',
)
static const String internalBuild = _Env.internalBuild;
}
-3
View File
@@ -1,12 +1,9 @@
import 'package:scaled_app/scaled_app.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'startup/startup.dart';
Future<void> main() async {
ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0);
VideoBlockKit.ensureInitialized();
await runAppFlowy();
}
@@ -0,0 +1,217 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:time/time.dart';
part 'notification_reminder_bloc.freezed.dart';
class NotificationReminderBloc
extends Bloc<NotificationReminderEvent, NotificationReminderState> {
NotificationReminderBloc() : super(NotificationReminderState.initial()) {
on<NotificationReminderEvent>((event, emit) async {
await event.when(
initial: (reminder, dateFormat, timeFormat) async {
this.reminder = reminder;
this.dateFormat = dateFormat;
this.timeFormat = timeFormat;
add(const NotificationReminderEvent.reset());
},
reset: () async {
final createdAt = await _getCreatedAt(
reminder,
dateFormat,
timeFormat,
);
final view = await _getView(reminder);
if (view == null) {
emit(
NotificationReminderState(
createdAt: createdAt,
pageTitle: '',
reminderContent: '',
status: NotificationReminderStatus.error,
),
);
}
final layout = view!.layout;
if (layout.isDocumentView) {
final node = await _getContent(reminder);
if (node != null) {
emit(
NotificationReminderState(
createdAt: createdAt,
pageTitle: view.name,
view: view,
reminderContent: node.delta?.toPlainText() ?? '',
nodes: [node],
status: NotificationReminderStatus.loaded,
),
);
}
} else if (layout.isDatabaseView) {
emit(
NotificationReminderState(
createdAt: createdAt,
pageTitle: view.name,
view: view,
reminderContent: reminder.message,
status: NotificationReminderStatus.loaded,
),
);
}
},
);
});
}
late final ReminderPB reminder;
late final UserDateFormatPB dateFormat;
late final UserTimeFormatPB timeFormat;
Future<String> _getCreatedAt(
ReminderPB reminder,
UserDateFormatPB dateFormat,
UserTimeFormatPB timeFormat,
) async {
final rCreatedAt = reminder.createdAt;
final createdAt = rCreatedAt != null
? _formatTimestamp(
rCreatedAt,
timeFormat: timeFormat,
dateFormate: dateFormat,
)
: '';
return createdAt;
}
Future<ViewPB?> _getView(ReminderPB reminder) async {
return ViewBackendService.getView(reminder.objectId)
.fold((s) => s, (_) => null);
}
Future<Node?> _getContent(ReminderPB reminder) async {
final blockId = reminder.meta[ReminderMetaKeys.blockId];
if (blockId == null) {
return null;
}
final document = await DocumentService()
.openDocument(
documentId: reminder.objectId,
)
.fold((s) => s.toDocument(), (_) => null);
if (document == null) {
return null;
}
final node = _searchById(document.root, blockId);
if (node == null) {
return null;
}
return node;
}
Node? _searchById(Node current, String id) {
if (current.id == id) {
return current;
}
if (current.children.isNotEmpty) {
for (final child in current.children) {
final node = _searchById(child, id);
if (node != null) {
return node;
}
}
}
return null;
}
String _formatTimestamp(
int timestamp, {
required UserDateFormatPB dateFormate,
required UserTimeFormatPB timeFormat,
}) {
final now = DateTime.now();
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final difference = now.difference(dateTime);
final String date;
if (difference.inMinutes < 1) {
date = LocaleKeys.sideBar_justNow.tr();
} else if (difference.inHours < 1 && dateTime.isToday) {
// Less than 1 hour
date = LocaleKeys.sideBar_minutesAgo
.tr(namedArgs: {'count': difference.inMinutes.toString()});
} else if (difference.inHours >= 1 && dateTime.isToday) {
// in same day
date = timeFormat.formatTime(dateTime);
} else {
date = dateFormate.formatDate(dateTime, false);
}
return date;
}
}
@freezed
class NotificationReminderEvent with _$NotificationReminderEvent {
const factory NotificationReminderEvent.initial(
ReminderPB reminder,
UserDateFormatPB dateFormat,
UserTimeFormatPB timeFormat,
) = _Initial;
const factory NotificationReminderEvent.reset() = _Reset;
}
enum NotificationReminderStatus {
initial,
loading,
loaded,
error,
}
@freezed
class NotificationReminderState with _$NotificationReminderState {
const NotificationReminderState._();
const factory NotificationReminderState({
required String createdAt,
required String pageTitle,
required String reminderContent,
@Default(NotificationReminderStatus.initial)
NotificationReminderStatus status,
@Default([]) List<Node> nodes,
ViewPB? view,
}) = _NotificationReminderState;
factory NotificationReminderState.initial() =>
const NotificationReminderState(
createdAt: '',
pageTitle: '',
reminderContent: '',
);
}
@@ -23,6 +23,9 @@ class DocumentPageStyleBloc
await event.when(
initial: () async {
try {
if (view.id.isEmpty) {
return;
}
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
@@ -146,7 +149,7 @@ class DocumentPageStyleBloc
) {
double padding = switch (fontLayout) {
PageStyleFontLayout.small => 1.0,
PageStyleFontLayout.normal => 2.0,
PageStyleFontLayout.normal => 1.0,
PageStyleFontLayout.large => 4.0,
};
switch (lineHeightLayout) {
@@ -162,6 +165,16 @@ class DocumentPageStyleBloc
return max(0, padding);
}
double calculateIconScale(
PageStyleFontLayout fontLayout,
) {
return switch (fontLayout) {
PageStyleFontLayout.small => 0.8,
PageStyleFontLayout.normal => 1.0,
PageStyleFontLayout.large => 1.2,
};
}
PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) {
final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ??
PageStyleFontLayout.normal.toString();
@@ -0,0 +1,54 @@
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:flutter/material.dart';
class AnimatedGestureDetector extends StatefulWidget {
const AnimatedGestureDetector({
super.key,
this.scaleFactor = 0.98,
this.feedback = true,
this.duration = const Duration(milliseconds: 100),
this.alignment = Alignment.center,
this.behavior = HitTestBehavior.opaque,
this.onTapUp,
required this.child,
});
final Widget child;
final double scaleFactor;
final Duration duration;
final Alignment alignment;
final bool feedback;
final HitTestBehavior behavior;
final VoidCallback? onTapUp;
@override
State<AnimatedGestureDetector> createState() =>
_AnimatedGestureDetectorState();
}
class _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {
double scale = 1.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: widget.behavior,
onTapUp: (details) {
setState(() => scale = 1.0);
HapticFeedbackType.light.call();
widget.onTapUp?.call();
},
onTapDown: (details) {
setState(() => scale = widget.scaleFactor);
},
child: AnimatedScale(
scale: scale,
alignment: widget.alignment,
duration: widget.duration,
child: widget.child,
),
);
}
}
@@ -46,10 +46,18 @@ class _MobileViewPageState extends State<MobileViewPage> {
// control the app bar opacity when in immersive mode
final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);
@override
void initState() {
super.initState();
getIt<ReminderBloc>().add(const ReminderEvent.started());
}
@override
void dispose() {
_appBarOpacity.dispose();
_scrollNotificationObserver = null;
super.dispose();
}
@@ -78,8 +86,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
value: getIt<ReminderBloc>(),
),
if (view.layout.isDocumentView)
BlocProvider(
@@ -125,7 +132,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
return child;
},
)
: child;
: SafeArea(child: child);
return Scaffold(
extendBodyBehindAppBar: isDocument,
appBar: appBar,
@@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
value.text,
fontSize: 14.0,
maxLines: 2,
lineHeight: 1.0,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
@@ -24,9 +24,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_document_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Document),
),
FlowyOptionTile.text(
@@ -34,9 +35,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_grid_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Grid),
),
FlowyOptionTile.text(
@@ -44,9 +46,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_board_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Board),
),
FlowyOptionTile.text(
@@ -54,9 +57,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_calendar_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Calendar),
),
FlowyOptionTile.text(
@@ -64,9 +68,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.chat_ai_page_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Chat),
),
],
@@ -33,6 +33,7 @@ class BlockActionBottomSheet extends StatelessWidget {
FlowySvgs.arrow_up_s,
size: Size.square(20),
),
showTopBorder: false,
onTap: () => onAction(BlockActionBottomSheetType.insertAbove),
),
FlowyOptionTile.text(
@@ -48,7 +49,7 @@ class BlockActionBottomSheet extends StatelessWidget {
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s),
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
),
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
@@ -6,6 +5,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -109,16 +109,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
await _showConfirmDialog(
onDelete: () {
recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));
fToast.showToast(
child: const _RemoveToast(),
positionedToastBuilder: (context, child) {
return Positioned.fill(
top: 450,
child: child,
);
},
);
},
);
}
@@ -136,38 +126,14 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
),
onRightButtonPressed: (context) {
onDelete();
Navigator.pop(context);
showToastNotification(
context,
message: LocaleKeys.sideBar_removeSuccess.tr(),
);
},
);
}
}
class _RemoveToast extends StatelessWidget {
const _RemoveToast();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: const Color(0xE5171717),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.success_s,
blendMode: null,
),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.sideBar_removeSuccess.tr(),
fontSize: 16.0,
color: Colors.white,
),
],
),
);
}
}
@@ -1,12 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -23,7 +24,7 @@ enum MobilePaneActionType {
MobileSlideActionButton actionButton(
BuildContext context, {
MobileViewCardType? cardType,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
}) {
switch (this) {
@@ -40,18 +41,32 @@ enum MobilePaneActionType {
backgroundColor: const Color(0xFFFA217F),
svg: FlowySvgs.favorite_section_remove_from_favorite_s,
size: 24.0,
onPressed: (context) => context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
);
case MobilePaneActionType.addToFavorites:
return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.favorite_s,
size: 24.0,
onPressed: (context) => context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
);
case MobilePaneActionType.add:
return MobileSlideActionButton(
@@ -69,6 +84,7 @@ enum MobilePaneActionType {
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
@@ -133,30 +149,27 @@ enum MobilePaneActionType {
List<MobileViewItemBottomSheetBodyAction> _buildActions(
ViewPB view, {
MobileViewCardType? cardType,
MobilePageCardType? cardType,
}) {
final isFavorite = view.isFavorite;
if (cardType != null) {
switch (cardType) {
case MobileViewCardType.recent:
case MobilePageCardType.recent:
return [
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
if (view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
case MobileViewCardType.favorite:
case MobilePageCardType.favorite:
return [
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
@@ -179,15 +192,15 @@ ActionPane buildEndActionPane(
BuildContext context,
List<MobilePaneActionType> actions, {
bool needSpace = true,
MobileViewCardType? cardType,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
required double spaceRatio,
}) {
debugPrint('actions: $actions');
return ActionPane(
motion: const ScrollMotion(),
extentRatio: actions.length / 5,
extentRatio: actions.length / spaceRatio,
children: [
if (needSpace) const HSpace(20),
if (needSpace) const HSpace(60),
...actions.map(
(action) => action.actionButton(
context,
@@ -70,6 +70,7 @@ Future<T?> showMobileBottomSheet<T>(
backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB)
: const Color(0xFF23262B);
barrierColor ??= Colors.black.withOpacity(0.3);
return showModalBottomSheet<T>(
context: context,
@@ -226,10 +227,14 @@ class BottomSheetHeader extends StatelessWidget {
),
),
Align(
child: FlowyText(
title,
fontSize: 16.0,
fontWeight: FontWeight.w500,
child: Container(
constraints: const BoxConstraints(maxWidth: 250),
child: FlowyText(
title,
fontSize: 17.0,
fontWeight: FontWeight.w500,
overflow: TextOverflow.ellipsis,
),
),
),
if (showDoneButton)
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -19,7 +20,10 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: double.infinity),
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: GridSize.headerHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -8,7 +10,6 @@ import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'mobile_create_field_screen.dart';
@@ -28,6 +29,7 @@ const mobileSupportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Checkbox,
FieldType.Checklist,
// FieldType.Time,
];
Future<FieldType?> showFieldTypeGridBottomSheet(
@@ -119,6 +119,7 @@ class FieldOptionValues {
case FieldType.RichText:
case FieldType.URL:
case FieldType.Checkbox:
case FieldType.Time:
return null;
case FieldType.Number:
return NumberTypeOptionPB(
@@ -133,7 +133,7 @@ enum DatabaseViewSettings {
filter => FlowySvgs.filter_s,
sort => FlowySvgs.sort_ascending_s,
board => FlowySvgs.board_s,
calendar => FlowySvgs.date_s,
calendar => FlowySvgs.calendar_s,
duplicate => FlowySvgs.copy_s,
delete => FlowySvgs.delete_s,
};
@@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
return Row(
children: [
FlowyText(
lineHeight: 1.0,
databaseLayoutFromViewLayout(view.layout).layoutName,
color: Theme.of(context).hintColor,
),
@@ -1,6 +1,7 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
@@ -68,7 +69,7 @@ class _MobileFavoriteSpaceState extends State<MobileFavoriteSpace>
if (favoriteState.views.isEmpty) {
return const EmptySpacePlaceholder(
type: MobileViewCardType.favorite,
type: MobilePageCardType.favorite,
);
}
@@ -92,11 +93,15 @@ class _FavoriteViews extends StatelessWidget {
@override
Widget build(BuildContext context) {
final borderColor = Theme.of(context).isLightMode
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return Scrollbar(
child: ListView.separated(
key: const PageStorageKey('favorite_views_page_storage_key'),
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
padding: EdgeInsets.only(
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
itemBuilder: (context, index) {
final view = favoriteViews[index];
@@ -105,16 +110,16 @@ class _FavoriteViews extends StatelessWidget {
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
color: borderColor,
width: 0.5,
),
),
),
child: MobileViewCard(
child: MobileViewPage(
key: ValueKey(view.item.id),
view: view.item,
timestamp: view.timestamp,
type: MobileViewCardType.favorite,
type: MobilePageCardType.favorite,
),
);
},
@@ -72,6 +72,7 @@ class MobileFavoriteFolder extends StatelessWidget {
MobilePaneActionType.more,
],
spaceType: FolderSpaceType.favorite,
spaceRatio: 5,
),
),
),
@@ -28,9 +28,10 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: HomeSpaceViewSizes.mVerticalPadding,
padding: EdgeInsets.only(
top: HomeSpaceViewSizes.mVerticalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
child: MobileFolders(
user: widget.userProfile,
@@ -1,12 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -30,78 +31,95 @@ class MobileFolders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
'';
return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial(
user,
workspaceId,
),
),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial(
user,
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
},
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) {
final lastCreatedRootView = state.lastCreatedRootView;
if (lastCreatedRootView != null) {
context.pushView(lastCreatedRootView);
}
},
builder: (context, state) {
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
return SlidableAutoCloseBehavior(
child: Column(
children: [
...isCollaborativeWorkspace
? [
MobileSectionFolder(
title: LocaleKeys.sideBar_workspace.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
spaceType: FolderSpaceType.private,
views: state.section.privateViews,
),
]
: [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
],
const VSpace(4.0),
const _TrashButton(),
],
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
},
),
),
context.read<SpaceBloc>().add(
SpaceEvent.reset(
user,
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
},
child: const _MobileFolder(),
);
}
}
class _MobileFolder extends StatefulWidget {
const _MobileFolder();
@override
State<_MobileFolder> createState() => _MobileFolderState();
}
class _MobileFolderState extends State<_MobileFolder> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) {
return SlidableAutoCloseBehavior(
child: Column(
children: [
..._buildSpaceOrSection(context, state),
const VSpace(4.0),
const Padding(
padding: EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _TrashButton(),
),
],
),
);
},
);
}
List<Widget> _buildSpaceOrSection(
BuildContext context,
SidebarSectionsState state,
) {
if (context.watch<SpaceBloc>().state.spaces.isNotEmpty) {
return [
const MobileSpace(),
];
}
if (context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn) {
return [
MobileSectionFolder(
title: LocaleKeys.sideBar_workspace.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
const VSpace(8.0),
MobileSectionFolder(
title: LocaleKeys.sideBar_private.tr(),
spaceType: FolderSpaceType.private,
views: state.section.privateViews,
),
];
}
return [
MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(),
spaceType: FolderSpaceType.public,
views: state.section.publicViews,
),
];
}
}
class _TrashButton extends StatelessWidget {
const _TrashButton();
@@ -5,8 +5,11 @@ import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
@@ -15,6 +18,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
@@ -57,6 +61,7 @@ class MobileHomeScreen extends StatelessWidget {
return Scaffold(
body: SafeArea(
bottom: false,
child: Provider.value(
value: userProfile,
child: MobileHomePage(
@@ -71,6 +76,9 @@ class MobileHomeScreen extends StatelessWidget {
}
}
final PropertyValueNotifier<UserWorkspacePB?> mCurrentWorkspace =
PropertyValueNotifier<UserWorkspacePB?>(null);
class MobileHomePage extends StatefulWidget {
const MobileHomePage({
super.key,
@@ -91,11 +99,13 @@ class _MobileHomePageState extends State<MobileHomePage> {
super.initState();
getIt<MenuSharedState>().addLatestViewListener(_onLatestViewChange);
getIt<ReminderBloc>().add(const ReminderEvent.started());
}
@override
void dispose() {
getIt<MenuSharedState>().removeLatestViewListener(_onLatestViewChange);
super.dispose();
}
@@ -111,17 +121,25 @@ class _MobileHomePageState extends State<MobileHomePage> {
create: (context) =>
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider.value(
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
),
],
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
listener: (context, state) => getIt<CachedRecentService>().reset(),
listener: (context, state) {
getIt<CachedRecentService>().reset();
mCurrentWorkspace.value = state.currentWorkspace;
},
builder: (context, state) {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
final workspaceId = state.currentWorkspace!.workspaceId;
return Column(
children: [
// Header
@@ -137,9 +155,36 @@ class _MobileHomePageState extends State<MobileHomePage> {
),
Expanded(
child: BlocProvider(
create: (context) =>
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SpaceOrderBloc()
..add(const SpaceOrderEvent.initial()),
),
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
widget.userProfile,
workspaceId,
),
),
),
BlocProvider(
create: (_) =>
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) => SpaceBloc()
..add(
SpaceEvent.initial(
widget.userProfile,
workspaceId,
openFirstPage: false,
),
),
),
],
child: MobileSpaceTab(
userProfile: widget.userProfile,
),
@@ -1,10 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
@@ -50,9 +51,10 @@ class MobileHomePageHeader extends StatelessWidget {
),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: FlowySvg(FlowySvgs.m_setting_m),
child: FlowySvg(FlowySvgs.m_notification_settings_s),
),
),
const HSpace(8.0),
],
),
);
@@ -113,8 +115,9 @@ class _MobileWorkspace extends StatelessWidget {
if (currentWorkspace == null) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: () {
return AnimatedGestureDetector(
alignment: Alignment.centerLeft,
onTapUp: () {
context.read<UserWorkspaceBloc>().add(
const UserWorkspaceEvent.fetchWorkspaces(),
);
@@ -130,6 +133,7 @@ class _MobileWorkspace extends StatelessWidget {
fontSize: 16.0,
enableEdit: false,
alignment: Alignment.centerLeft,
figmaLineHeight: 16.0,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
@@ -143,7 +147,7 @@ class _MobileWorkspace extends StatelessWidget {
: const HSpace(8),
FlowyText.semibold(
currentWorkspace.name,
fontSize: 16.0,
fontSize: 20.0,
overflow: TextOverflow.ellipsis,
),
],
@@ -162,9 +166,10 @@ class _MobileWorkspace extends StatelessWidget {
showHeader: true,
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
title: LocaleKeys.workspace_menuTitle.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
builder: (sheetContext) {
return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
@@ -179,7 +184,7 @@ class _MobileWorkspace extends StatelessWidget {
currentWorkspace: currentWorkspace,
workspaces: workspaces,
onWorkspaceSelected: (workspace) {
context.pop();
Navigator.of(sheetContext).pop();
if (workspace == currentWorkspace) {
return;
@@ -38,8 +38,7 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toList();
List<ViewPB> recentViews = state.views.map((e) => e.item).toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.
@@ -1,5 +1,6 @@
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/recent/prelude.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -36,7 +37,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
if (recentViews.isEmpty) {
return const Center(
child: EmptySpacePlaceholder(type: MobileViewCardType.recent),
child: EmptySpacePlaceholder(type: MobilePageCardType.recent),
);
}
@@ -48,7 +49,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
final ids = <String>{};
final filteredRecentViews = recentViews.reversed.toList();
final filteredRecentViews = recentViews.toList();
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
return filteredRecentViews;
}
@@ -63,12 +64,16 @@ class _RecentViews extends StatelessWidget {
@override
Widget build(BuildContext context) {
final borderColor = Theme.of(context).isLightMode
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return SlidableAutoCloseBehavior(
child: Scrollbar(
child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'),
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
padding: EdgeInsets.only(
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
itemBuilder: (context, index) {
final sectionView = recentViews[index];
@@ -77,16 +82,16 @@ class _RecentViews extends StatelessWidget {
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
color: borderColor,
width: 0.5,
),
),
),
child: MobileViewCard(
child: MobileViewPage(
key: ValueKey(sectionView.item.id),
view: sectionView.item,
timestamp: sectionView.timestamp,
type: MobileViewCardType.recent,
type: MobilePageCardType.recent,
),
);
},
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
@@ -43,47 +43,17 @@ class MobileSectionFolder extends StatelessWidget {
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName
.tr(),
index: 0,
viewSection: spaceType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
},
onAdded: () => _createNewPage(context),
),
),
if (state.isExpanded)
...views.map(
(view) => MobileViewItem(
key: ValueKey(
'${FolderSpaceType.private.name} ${view.id}',
),
Padding(
padding: const EdgeInsets.only(
left: HomeSpaceViewSizes.leftPadding,
),
child: _Pages(
views: views,
spaceType: spaceType,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
spaceType: spaceType,
needSpace: false,
);
},
),
),
],
@@ -92,4 +62,63 @@ class MobileSectionFolder extends StatelessWidget {
),
);
}
void _createNewPage(BuildContext context) {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: spaceType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
}
}
class _Pages extends StatelessWidget {
const _Pages({
required this.views,
required this.spaceType,
});
final List<ViewPB> views;
final FolderSpaceType spaceType;
@override
Widget build(BuildContext context) {
return Column(
children: views
.map(
(view) => MobileViewItem(
key: ValueKey(
'${FolderSpaceType.private.name} ${view.id}',
),
spaceType: spaceType,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
spaceType: spaceType,
needSpace: false,
spaceRatio: 5,
);
},
),
)
.toList(),
);
}
}
@@ -32,6 +32,7 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
Widget build(BuildContext context) {
return Row(
children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(
child: FlowyButton(
text: FlowyText.medium(
@@ -57,15 +58,19 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
},
),
),
FlowyIconButton(
key: mobileCreateNewPageButtonKey,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
height: HomeSpaceViewSizes.mViewButtonDimension,
width: HomeSpaceViewSizes.mViewButtonDimension,
icon: const FlowySvg(
FlowySvgs.m_space_add_s,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.onAdded,
child: Container(
// expand the touch area
margin: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: 8.0,
),
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
),
onPressed: widget.onAdded,
),
],
);
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
class EmptySpacePlaceholder extends StatelessWidget {
const EmptySpacePlaceholder({super.key, required this.type});
final MobileViewCardType type;
final MobilePageCardType type;
@override
Widget build(BuildContext context) {
@@ -42,14 +42,14 @@ class EmptySpacePlaceholder extends StatelessWidget {
}
String get _emptyPageText => switch (type) {
MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
MobilePageCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
MobilePageCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
};
String get _emptyPageSubText => switch (type) {
MobileViewCardType.recent =>
MobilePageCardType.recent =>
LocaleKeys.sideBar_emptyRecentDescription.tr(),
MobileViewCardType.favorite =>
MobilePageCardType.favorite =>
LocaleKeys.sideBar_emptyFavoriteDescription.tr(),
};
}
@@ -5,20 +5,23 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -26,19 +29,20 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
import 'package:time/time.dart';
enum MobileViewCardType {
enum MobilePageCardType {
recent,
favorite;
String get lastOperationHintText => switch (this) {
MobileViewCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),
MobileViewCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),
MobilePageCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),
MobilePageCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),
};
}
class MobileViewCard extends StatelessWidget {
const MobileViewCard({
class MobileViewPage extends StatelessWidget {
const MobileViewPage({
super.key,
required this.view,
this.timestamp,
@@ -47,7 +51,7 @@ class MobileViewCard extends StatelessWidget {
final ViewPB view;
final Int64? timestamp;
final MobileViewCardType type;
final MobilePageCardType type;
@override
Widget build(BuildContext context) {
@@ -74,14 +78,14 @@ class MobileViewCard extends StatelessWidget {
: MobilePaneActionType.addToFavorites,
],
cardType: type,
spaceRatio: 4,
),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: (_) => context.pushView(view),
onLongPressUp: () => _showActionSheet(context),
child: AnimatedGestureDetector(
onTapUp: () => context.pushView(view),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(child: _buildDescription(context, state)),
const HSpace(20.0),
SizedBox(
@@ -89,6 +93,7 @@ class MobileViewCard extends StatelessWidget {
height: 60,
child: _buildCover(context, state),
),
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
],
),
),
@@ -157,6 +162,7 @@ class MobileViewCard extends StatelessWidget {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _ViewCover(
layout: view.layout,
coverTypeV1: state.coverTypeV1,
coverTypeV2: state.coverTypeV2,
value: state.coverValue,
@@ -189,6 +195,7 @@ class MobileViewCard extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w600,
height: 1.3,
),
),
],
@@ -207,40 +214,45 @@ class MobileViewCard extends StatelessWidget {
}
Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode
? const Color(0x7F171717)
: Colors.white.withOpacity(0.45);
if (timestamp == null) {
return const SizedBox.shrink();
}
final date = _formatTimestamp(
context,
timestamp!.toInt() * 1000,
);
return FlowyText.regular(
date,
fontSize: 12.0,
color: Theme.of(context).hintColor,
fontSize: 13.0,
color: textColor,
);
}
String _formatTimestamp(int timestamp) {
String _formatTimestamp(BuildContext context, int timestamp) {
final now = DateTime.now();
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final difference = now.difference(dateTime);
final String date;
final dateFormate =
context.read<AppearanceSettingsCubit>().state.dateFormat;
final timeFormate =
context.read<AppearanceSettingsCubit>().state.timeFormat;
if (difference.inMinutes < 1) {
date = LocaleKeys.sideBar_justNow.tr();
} else if (difference.inHours < 1) {
} else if (difference.inHours < 1 && dateTime.isToday) {
// Less than 1 hour
date = LocaleKeys.sideBar_minutesAgo
.tr(namedArgs: {'count': difference.inMinutes.toString()});
} else if (difference.inHours >= 1 && difference.inHours < 24) {
// Between 1 hour and 24 hours
date = DateFormat('h:mm a').format(dateTime);
} else if (difference.inDays >= 1 && dateTime.year == now.year) {
// More than 24 hours but within the current year
date = DateFormat('M/d, h:mm a').format(dateTime);
} else if (difference.inHours >= 1 && dateTime.isToday) {
// in same day
date = timeFormate.formatTime(dateTime);
} else {
// Other cases (previous years)
date = DateFormat('M/d/yyyy, h:mm a').format(dateTime);
date = dateFormate.formatDate(dateTime, false);
}
if (difference.inHours >= 1) {
@@ -249,77 +261,24 @@ class MobileViewCard extends StatelessWidget {
return date;
}
Future<void> _showActionSheet(BuildContext context) async {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
final recentViewsBloc = context.read<RecentViewsBloc?>();
await showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: AFThemeExtension.of(context).background,
useRootNavigator: true,
builder: (context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: viewBloc),
BlocProvider.value(value: favoriteBloc),
if (recentViewsBloc != null)
BlocProvider.value(value: recentViewsBloc),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return MobileViewItemBottomSheet(
view: viewBloc.state.view,
actions: _buildActions(state.view),
);
},
),
);
},
);
}
List<MobileViewItemBottomSheetBodyAction> _buildActions(ViewPB view) {
switch (type) {
case MobileViewCardType.recent:
return [
view.isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
if (view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
case MobileViewCardType.favorite:
return [
MobileViewItemBottomSheetBodyAction.removeFromFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
}
class _ViewCover extends StatelessWidget {
const _ViewCover({
required this.layout,
required this.coverTypeV1,
this.coverTypeV2,
this.value,
});
final ViewLayoutPB layout;
final CoverType coverTypeV1;
final PageStyleCoverImageType? coverTypeV2;
final String? value;
@override
Widget build(BuildContext context) {
final placeholder = Container(
color: const Color(0xFFE1FBFF),
);
final placeholder = _buildPlaceholder(context);
final value = this.value;
if (value == null) {
return placeholder;
@@ -330,6 +289,45 @@ class _ViewCover extends StatelessWidget {
return _buildCoverV1(context, value, placeholder);
}
Widget _buildPlaceholder(BuildContext context) {
final isLightMode = Theme.of(context).isLightMode;
final (svg, color) = switch (layout) {
ViewLayoutPB.Document => (
FlowySvgs.m_document_thumbnail_m,
isLightMode ? const Color(0xCCEDFBFF) : const Color(0x33658B90)
),
ViewLayoutPB.Grid => (
FlowySvgs.m_grid_thumbnail_m,
isLightMode ? const Color(0xFFF5F4FF) : const Color(0x338B80AD)
),
ViewLayoutPB.Board => (
FlowySvgs.m_board_thumbnail_m,
isLightMode ? const Color(0x7FE0FDD9) : const Color(0x3372936B),
),
ViewLayoutPB.Calendar => (
FlowySvgs.m_calendar_thumbnail_m,
isLightMode ? const Color(0xFFFFF7F0) : const Color(0x33A68B77)
),
ViewLayoutPB.Chat => (
FlowySvgs.m_chat_thumbnail_m,
isLightMode ? const Color(0x66FFE6FD) : const Color(0x33987195)
),
_ => (
FlowySvgs.m_document_thumbnail_m,
isLightMode ? Colors.black : Colors.white
)
};
return ColoredBox(
color: color,
child: Center(
child: FlowySvg(
svg,
blendMode: null,
),
),
);
}
Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {
final type = coverTypeV2;
if (type == null) {
@@ -0,0 +1,163 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSpace extends StatelessWidget {
const MobileSpace({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
if (state.spaces.isEmpty) {
return const SizedBox.shrink();
}
final currentSpace = state.currentSpace ?? state.spaces.first;
return Column(
children: [
MobileSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
onAdded: () => _showCreatePageMenu(context, currentSpace),
onPressed: () => _showSpaceMenu(context),
),
Padding(
padding: const EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
),
),
],
);
},
);
}
void _showSpaceMenu(BuildContext context) {
showMobileBottomSheet(
context,
showDivider: false,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
showDoneButton: true,
useRootNavigator: true,
title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
return BlocProvider.value(
value: context.read<SpaceBloc>(),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: MobileSpaceMenu(),
),
);
},
);
}
void _showCreatePageMenu(BuildContext context, ViewPB space) {
final title = space.name;
showMobileBottomSheet(
context,
showHeader: true,
title: title,
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
view: space,
onAction: (layout) {
Navigator.of(sheetContext).pop();
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
index: 0,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(space, true),
);
},
);
},
);
}
}
class _Pages extends StatelessWidget {
const _Pages({
super.key,
required this.space,
});
final ViewPB space;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ViewBloc(view: space)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final spaceType = space.spacePermission == SpacePermission.publicToAll
? FolderSpaceType.public
: FolderSpaceType.private;
return Column(
children: state.view.childViews
.map(
(view) => MobileViewItem(
key: ValueKey(
'${space.id} ${view.id}',
),
spaceType: spaceType,
isFirstChild: view.id == state.view.childViews.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
final actions = [
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
];
return buildEndActionPane(
context,
actions,
spaceType: spaceType,
spaceRatio: actions.length == 1 ? 3 : 4,
);
},
),
)
.toList(),
);
},
),
);
}
}
@@ -0,0 +1,143 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
class MobileSpaceHeader extends StatelessWidget {
const MobileSpaceHeader({
super.key,
required this.space,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
final ViewPB space;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onPressed,
child: SizedBox(
height: 48,
child: Row(
children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
SpaceIcon(
dimension: 24,
space: space,
svgSize: 14,
textDimension: 18.0,
cornerRadius: 6.0,
),
const HSpace(8),
FlowyText.medium(
space.name,
lineHeight: 1.15,
fontSize: 16.0,
),
const HSpace(4.0),
const FlowySvg(
FlowySvgs.workspace_drop_down_menu_show_s,
),
const Spacer(),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onAdded,
child: Container(
// expand the touch area
margin: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: 8.0,
),
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
),
),
],
),
),
);
}
// Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {
// switch (type) {
// case SpaceMoreActionType.rename:
// await _showRenameDialog();
// break;
// case SpaceMoreActionType.changeIcon:
// final (String icon, String iconColor) = data;
// context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));
// break;
// case SpaceMoreActionType.manage:
// _showManageSpaceDialog(context);
// break;
// case SpaceMoreActionType.addNewSpace:
// break;
// case SpaceMoreActionType.collapseAllPages:
// break;
// case SpaceMoreActionType.delete:
// _showDeleteSpaceDialog(context);
// break;
// case SpaceMoreActionType.divider:
// break;
// }
// }
// Future<void> _showRenameDialog() async {
// await NavigatorTextFieldDialog(
// title: LocaleKeys.space_rename.tr(),
// value: space.name,
// autoSelectAllText: true,
// onConfirm: (name, _) {
// context.read<SpaceBloc>().add(SpaceEvent.rename(space, name));
// },
// ).show(context);
// }
// void _showManageSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const ManageSpacePopup(),
// ),
// );
// },
// );
// }
// void _showDeleteSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const SizedBox(width: 440, child: DeleteSpacePopup()),
// ),
// );
// },
// );
// }
}
@@ -0,0 +1,132 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSpaceMenu extends StatelessWidget {
const MobileSpaceMenu({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(4.0),
for (final space in state.spaces)
SizedBox(
height: 52,
child: _SidebarSpaceMenuItem(
space: space,
isSelected: state.currentSpace?.id == space.id,
),
),
// const Padding(
// padding: EdgeInsets.symmetric(vertical: 8.0),
// child: Divider(
// height: 0.5,
// ),
// ),
// const SizedBox(
// height: 52,
// child: _CreateSpaceButton(),
// ),
],
);
},
);
}
}
class _SidebarSpaceMenuItem extends StatelessWidget {
const _SidebarSpaceMenuItem({
required this.space,
required this.isSelected,
});
final ViewPB space;
final bool isSelected;
@override
Widget build(BuildContext context) {
return FlowyButton(
text: Row(
children: [
FlowyText.medium(
space.name,
fontSize: 16.0,
),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
const FlowySvg(
FlowySvgs.space_lock_s,
size: Size.square(12),
),
],
),
margin: const EdgeInsets.symmetric(horizontal: 12.0),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 24,
space: space,
svgSize: 14,
textDimension: 18.0,
cornerRadius: 6.0,
),
leftIconSize: const Size.square(24),
rightIcon: isSelected
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () {
context.read<SpaceBloc>().add(SpaceEvent.open(space));
Navigator.of(context).pop();
},
);
}
}
// class _CreateSpaceButton extends StatelessWidget {
// const _CreateSpaceButton();
// @override
// Widget build(BuildContext context) {
// return FlowyButton(
// text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),
// iconPadding: 10,
// leftIcon: const FlowySvg(
// FlowySvgs.space_add_s,
// ),
// leftIconSize: const Size.square(20),
// onTap: () {
// PopoverContainer.of(context).close();
// _showCreateSpaceDialog(context);
// },
// );
// }
// void _showCreateSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const CreateSpacePopup(),
// ),
// );
// },
// );
// }
// }
@@ -22,11 +22,13 @@ class MobileSpaceTabBar extends StatelessWidget {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
final labelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15.0,
fontSize: 16.0,
height: 22.0 / 16.0,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 15.0,
height: 22.0 / 15.0,
);
return Container(
@@ -1,16 +1,27 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart';
import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class MobileSpaceTab extends StatefulWidget {
const MobileSpaceTab({super.key, required this.userProfile});
const MobileSpaceTab({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@@ -22,10 +33,19 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
with SingleTickerProviderStateMixin {
TabController? tabController;
@override
void initState() {
super.initState();
mobileCreateNewPageNotifier.addListener(_createNewPage);
}
@override
void dispose() {
tabController?.removeListener(_onTabChange);
tabController?.dispose();
mobileCreateNewPageNotifier.removeListener(_createNewPage);
super.dispose();
}
@@ -33,36 +53,60 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
Widget build(BuildContext context) {
return Provider.value(
value: widget.userProfile,
child: BlocBuilder<SpaceOrderBloc, SpaceOrderState>(
builder: (context, state) {
if (state.isLoading) {
return const SizedBox.shrink();
}
child: MultiBlocListener(
listeners: [
BlocListener<SpaceBloc, SpaceState>(
listenWhen: (p, c) =>
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
listener: (context, state) {
final lastCreatedPage = state.lastCreatedPage;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
}
},
),
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) {
final lastCreatedPage = state.lastCreatedRootView;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
}
},
),
],
child: BlocBuilder<SpaceOrderBloc, SpaceOrderState>(
builder: (context, state) {
if (state.isLoading) {
return const SizedBox.shrink();
}
_initTabController(state);
_initTabController(state);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileSpaceTabBar(
tabController: tabController!,
tabs: state.tabsOrder,
onReorder: (from, to) {
context.read<SpaceOrderBloc>().add(
SpaceOrderEvent.reorder(from, to),
);
},
),
const HSpace(12.0),
Expanded(
child: TabBarView(
controller: tabController,
children: _buildTabs(state),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileSpaceTabBar(
tabController: tabController!,
tabs: state.tabsOrder,
onReorder: (from, to) {
context.read<SpaceOrderBloc>().add(
SpaceOrderEvent.reorder(from, to),
);
},
),
),
],
);
},
const HSpace(12.0),
Expanded(
child: TabBarView(
controller: tabController,
children: _buildTabs(state),
),
),
],
);
},
),
),
);
}
@@ -104,4 +148,27 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
}
}).toList();
}
// quick create new page when clicking the add button in navigation bar
void _createNewPage() {
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
),
);
} else {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: FolderSpaceType.public.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
}
}
}
@@ -138,17 +138,21 @@ class _WorkspaceMenuItem extends StatelessWidget {
height: 60,
showTopBorder: showTopBorder,
showBottomBorder: false,
leftIcon: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
fontSize: 16.0,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
leftIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
fontSize: 16.0,
figmaLineHeight: 16.0,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
@@ -1,15 +1,32 @@
import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
import 'package:appflowy/shared/red_dot.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
enum BottomNavigationBarActionType {
home,
notificationMultiSelect,
}
final PropertyValueNotifier<ViewLayoutPB?> mobileCreateNewPageNotifier =
PropertyValueNotifier(null);
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
ValueNotifier(BottomNavigationBarActionType.home);
const _homeLabel = 'home';
const _addLabel = 'add';
const _notificationLabel = 'notification';
@@ -25,16 +42,16 @@ final _items = <BottomNavigationBarItem>[
),
const BottomNavigationBarItem(
label: _notificationLabel,
icon: FlowySvg(FlowySvgs.m_home_notification_m),
activeIcon: FlowySvg(
FlowySvgs.m_home_notification_m,
icon: _NotificationNavigationBarItemIcon(),
activeIcon: _NotificationNavigationBarItemIcon(
isActive: true,
),
),
];
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class MobileBottomNavigationBar extends StatelessWidget {
class MobileBottomNavigationBar extends StatefulWidget {
/// Constructs an [MobileBottomNavigationBar].
const MobileBottomNavigationBar({
required this.navigationShell,
@@ -44,24 +61,149 @@ class MobileBottomNavigationBar extends StatelessWidget {
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
@override
State<MobileBottomNavigationBar> createState() =>
_MobileBottomNavigationBarState();
}
class _MobileBottomNavigationBarState extends State<MobileBottomNavigationBar> {
Widget? _bottomNavigationBar;
@override
void initState() {
super.initState();
bottomNavigationBarType.addListener(_animate);
}
@override
void dispose() {
bottomNavigationBarType.removeListener(_animate);
super.dispose();
}
@override
Widget build(BuildContext context) {
_bottomNavigationBar = switch (bottomNavigationBarType.value) {
BottomNavigationBarActionType.home =>
_buildHomePageNavigationBar(context),
BottomNavigationBarActionType.notificationMultiSelect =>
_buildNotificationNavigationBar(context),
};
return Scaffold(
body: navigationShell,
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
body: widget.navigationShell,
extendBody: true,
bottomNavigationBar: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: _transitionBuilder,
child: _bottomNavigationBar,
),
);
}
Widget _buildHomePageNavigationBar(BuildContext context) {
return _HomePageNavigationBar(
navigationShell: widget.navigationShell,
);
}
Widget _buildNotificationNavigationBar(BuildContext context) {
return const _NotificationNavigationBar();
}
// widget A going down, widget B going up
Widget _transitionBuilder(
Widget child,
Animation<double> animation,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: child,
);
}
void _animate() {
setState(() {});
}
}
class _NotificationNavigationBarItemIcon extends StatelessWidget {
const _NotificationNavigationBarItemIcon({
this.isActive = false,
});
final bool isActive;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: getIt<ReminderBloc>(),
child: BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
final hasUnreads = state.reminders.any(
(reminder) => !reminder.isRead,
);
return Stack(
children: [
isActive
? const FlowySvg(
FlowySvgs.m_home_active_notification_m,
blendMode: null,
)
: const FlowySvg(
FlowySvgs.m_home_notification_m,
),
if (hasUnreads)
const Positioned(
top: 2,
right: 4,
child: NotificationRedDot(),
),
],
);
},
),
);
}
}
class _HomePageNavigationBar extends StatelessWidget {
const _HomePageNavigationBar({
required this.navigationShell,
});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 3,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: _items,
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
child: DecoratedBox(
decoration: BoxDecoration(
border: context.border,
color: context.backgroundColor,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: _items,
backgroundColor: Colors.transparent,
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
),
),
),
);
@@ -70,10 +212,13 @@ class MobileBottomNavigationBar extends StatelessWidget {
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int bottomBarIndex) {
if (_items[bottomBarIndex].label == _addLabel) {
final label = _items[bottomBarIndex].label;
if (label == _addLabel) {
// show an add dialog
_showCreatePageBottomSheet(context);
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
return;
} else if (label == _notificationLabel) {
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
}
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
@@ -87,40 +232,113 @@ class MobileBottomNavigationBar extends StatelessWidget {
initialLocation: bottomBarIndex == navigationShell.currentIndex,
);
}
}
void _showCreatePageBottomSheet(BuildContext context) {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.sideBar_addAPage.tr(),
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
view: ViewPB(),
onAction: (layout) async {
Navigator.of(sheetContext).pop();
final currentWorkspaceId =
await FolderEventReadCurrentWorkspace().send();
await currentWorkspaceId.fold((s) async {
final workspaceService = WorkspaceService(workspaceId: s.id);
final result = await workspaceService.createView(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
viewSection: ViewSectionPB.Private,
layout: layout,
);
result.fold((s) {
context.pushView(s);
}, (e) {
Log.error('Failed to create new page: $e');
});
}, (e) {
Log.error('Failed to read current workspace: $e');
});
},
);
},
class _NotificationNavigationBar extends StatelessWidget {
const _NotificationNavigationBar();
@override
Widget build(BuildContext context) {
return Container(
// todo: use real height here.
height: 90,
decoration: BoxDecoration(
border: context.border,
color: context.backgroundColor,
),
padding: const EdgeInsets.only(bottom: 20),
child: ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (context, value, child) {
if (value.isEmpty) {
// not editable
return IgnorePointer(
child: Opacity(
opacity: 0.3,
child: child,
),
);
}
return child!;
},
child: Row(
children: [
const HSpace(20),
Expanded(
child: NavigationBarButton(
icon: FlowySvgs.m_notification_action_mark_as_read_s,
text: LocaleKeys.settings_notifications_action_markAsRead.tr(),
onTap: () => _onMarkAsRead(context),
),
),
const HSpace(16),
Expanded(
child: NavigationBarButton(
icon: FlowySvgs.m_notification_action_archive_s,
text: LocaleKeys.settings_notifications_action_archive.tr(),
onTap: () => _onArchive(context),
),
),
const HSpace(20),
],
),
),
);
}
void _onMarkAsRead(BuildContext context) {
if (mSelectedNotificationIds.value.isEmpty) {
return;
}
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_allSuccess
.tr(),
);
getIt<ReminderBloc>()
.add(ReminderEvent.markAsRead(mSelectedNotificationIds.value));
mSelectedNotificationIds.value = [];
}
void _onArchive(BuildContext context) {
if (mSelectedNotificationIds.value.isEmpty) {
return;
}
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
getIt<ReminderBloc>()
.add(ReminderEvent.archive(mSelectedNotificationIds.value));
mSelectedNotificationIds.value = [];
}
}
extension on BuildContext {
Color get backgroundColor {
return Theme.of(this).isLightMode
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
}
Color get borderColor {
return Theme.of(this).isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
}
Border? get border {
return Theme.of(this).isLightMode
? Border(top: BorderSide(color: borderColor))
: null;
}
}
@@ -0,0 +1,59 @@
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileNotificationsMultiSelectScreen extends StatelessWidget {
const MobileNotificationsMultiSelectScreen({super.key});
static const routeName = '/notifications_multi_select';
@override
Widget build(BuildContext context) {
return BlocProvider<ReminderBloc>.value(
value: getIt<ReminderBloc>(),
child: const MobileNotificationMultiSelect(),
);
}
}
class MobileNotificationMultiSelect extends StatefulWidget {
const MobileNotificationMultiSelect({
super.key,
});
@override
State<MobileNotificationMultiSelect> createState() =>
_MobileNotificationMultiSelectState();
}
class _MobileNotificationMultiSelectState
extends State<MobileNotificationMultiSelect> {
@override
void dispose() {
mSelectedNotificationIds.value.clear();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileNotificationMultiSelectPageHeader(),
VSpace(12.0),
Expanded(
child: MultiSelectNotificationTab(),
),
],
),
),
);
}
}
@@ -0,0 +1,129 @@
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
final PropertyValueNotifier<List<String>> mSelectedNotificationIds =
PropertyValueNotifier([]);
class MobileNotificationsScreenV2 extends StatefulWidget {
const MobileNotificationsScreenV2({super.key});
static const routeName = '/notifications';
@override
State<MobileNotificationsScreenV2> createState() =>
_MobileNotificationsScreenV2State();
}
class _MobileNotificationsScreenV2State
extends State<MobileNotificationsScreenV2>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
mCurrentWorkspace.addListener(_onRefresh);
}
@override
void dispose() {
mCurrentWorkspace.removeListener(_onRefresh);
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return BlocProvider<ReminderBloc>.value(
value: getIt<ReminderBloc>(),
child: ValueListenableBuilder(
valueListenable: bottomNavigationBarType,
builder: (_, value, __) {
switch (value) {
case BottomNavigationBarActionType.home:
return const MobileNotificationsTab();
case BottomNavigationBarActionType.notificationMultiSelect:
return const MobileNotificationMultiSelect();
}
},
),
);
}
void _onRefresh() {
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
}
}
class MobileNotificationsTab extends StatefulWidget {
const MobileNotificationsTab({
super.key,
});
@override
State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();
}
class _MobileNotificationsTabState extends State<MobileNotificationsTab>
with SingleTickerProviderStateMixin {
late TabController tabController;
final tabs = [
MobileNotificationTabType.inbox,
MobileNotificationTabType.unread,
MobileNotificationTabType.archive,
];
@override
void initState() {
super.initState();
tabController = TabController(
length: 3,
vsync: this,
);
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MobileNotificationPageHeader(),
MobileNotificationTabBar(
tabController: tabController,
tabs: tabs,
),
const VSpace(12.0),
Expanded(
child: TabBarView(
controller: tabController,
children: tabs.map((e) => NotificationTab(tabType: e)).toList(),
),
),
],
),
),
);
}
}
@@ -0,0 +1,11 @@
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
extension NotificationItemColors on BuildContext {
Color get notificationItemTextColor {
if (Theme.of(this).isLightMode) {
return const Color(0xFF171717);
}
return const Color(0xFFffffff).withOpacity(0.8);
}
}
@@ -0,0 +1,58 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmptyNotification extends StatelessWidget {
const EmptyNotification({
super.key,
required this.type,
});
final MobileNotificationTabType type;
@override
Widget build(BuildContext context) {
final title = switch (type) {
MobileNotificationTabType.inbox =>
LocaleKeys.settings_notifications_emptyInbox_title.tr(),
MobileNotificationTabType.archive =>
LocaleKeys.settings_notifications_emptyArchived_title.tr(),
MobileNotificationTabType.unread =>
LocaleKeys.settings_notifications_emptyUnread_title.tr(),
};
final desc = switch (type) {
MobileNotificationTabType.inbox =>
LocaleKeys.settings_notifications_emptyInbox_description.tr(),
MobileNotificationTabType.archive =>
LocaleKeys.settings_notifications_emptyArchived_description.tr(),
MobileNotificationTabType.unread =>
LocaleKeys.settings_notifications_emptyUnread_description.tr(),
};
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(FlowySvgs.m_empty_notification_xl),
const VSpace(12.0),
FlowyText(
title,
fontSize: 16.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
),
const VSpace(4.0),
Opacity(
opacity: 0.45,
child: FlowyText(
desc,
fontSize: 15.0,
figmaLineHeight: 22.0,
fontWeight: FontWeight.w400,
),
),
],
);
}
}
@@ -0,0 +1,96 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/settings_popup_menu.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileNotificationPageHeader extends StatelessWidget {
const MobileNotificationPageHeader({
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 56),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(16.0),
FlowyText(
LocaleKeys.settings_notifications_titles_notifications.tr(),
fontSize: 20,
fontWeight: FontWeight.w600,
),
const Spacer(),
const NotificationSettingsPopupMenu(),
const HSpace(16.0),
],
),
);
}
}
class MobileNotificationMultiSelectPageHeader extends StatelessWidget {
const MobileNotificationMultiSelectPageHeader({
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 56),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildCancelButton(
isOpaque: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () => bottomNavigationBarType.value =
BottomNavigationBarActionType.home,
),
ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (_, value, __) {
return FlowyText(
// todo: i18n
'${value.length} Selected',
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
);
},
),
// this button is used to align the text to the center
_buildCancelButton(
isOpaque: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
),
],
),
);
}
//
Widget _buildCancelButton({
required bool isOpaque,
required EdgeInsets padding,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: padding,
child: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: isOpaque ? Colors.transparent : null,
),
),
);
}
}
@@ -0,0 +1,115 @@
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MultiSelectNotificationItem extends StatelessWidget {
const MultiSelectNotificationItem({
super.key,
required this.reminder,
});
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
final settings = context.read<AppearanceSettingsCubit>().state;
final dateFormate = settings.dateFormat;
final timeFormate = settings.timeFormat;
return BlocProvider<NotificationReminderBloc>(
create: (context) => NotificationReminderBloc()
..add(
NotificationReminderEvent.initial(
reminder,
dateFormate,
timeFormate,
),
),
child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(
builder: (context, state) {
if (state.status == NotificationReminderStatus.loading ||
state.status == NotificationReminderStatus.initial) {
return const SizedBox.shrink();
}
if (state.status == NotificationReminderStatus.error) {
// error handle.
return const SizedBox.shrink();
}
final child = ValueListenableBuilder(
valueListenable: mSelectedNotificationIds,
builder: (_, selectedIds, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12),
decoration: selectedIds.contains(reminder.id)
? ShapeDecoration(
color: const Color(0x1900BCF0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
)
: null,
child: child,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _InnerNotificationItem(
reminder: reminder,
),
),
);
return AnimatedGestureDetector(
scaleFactor: 0.99,
onTapUp: () {
if (mSelectedNotificationIds.value.contains(reminder.id)) {
mSelectedNotificationIds.value = mSelectedNotificationIds.value
..remove(reminder.id);
} else {
mSelectedNotificationIds.value = mSelectedNotificationIds.value
..add(reminder.id);
}
},
child: child,
);
},
),
);
}
}
class _InnerNotificationItem extends StatelessWidget {
const _InnerNotificationItem({
required this.reminder,
});
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const HSpace(10.0),
NotificationCheckIcon(
isSelected: mSelectedNotificationIds.value.contains(reminder.id),
),
const HSpace(3.0),
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
const HSpace(3.0),
NotificationIcon(reminder: reminder),
const HSpace(12.0),
Expanded(
child: NotificationContent(reminder: reminder),
),
],
);
}
}
@@ -0,0 +1,164 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class NotificationItem extends StatelessWidget {
const NotificationItem({
super.key,
required this.tabType,
required this.reminder,
});
final MobileNotificationTabType tabType;
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
final settings = context.read<AppearanceSettingsCubit>().state;
final dateFormate = settings.dateFormat;
final timeFormate = settings.timeFormat;
return BlocProvider<NotificationReminderBloc>(
create: (context) => NotificationReminderBloc()
..add(
NotificationReminderEvent.initial(
reminder,
dateFormate,
timeFormate,
),
),
child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(
builder: (context, state) {
if (state.status == NotificationReminderStatus.loading ||
state.status == NotificationReminderStatus.initial) {
return const SizedBox.shrink();
}
if (state.status == NotificationReminderStatus.error) {
// error handle.
return const SizedBox.shrink();
}
final child = Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _SlidableNotificationItem(
tabType: tabType,
reminder: reminder,
child: _InnerNotificationItem(
tabType: tabType,
reminder: reminder,
),
),
);
return AnimatedGestureDetector(
scaleFactor: 0.99,
child: child,
onTapUp: () async {
final view = state.view;
if (view == null) {
return;
}
await context.pushView(view);
if (!reminder.isRead && context.mounted) {
context.read<ReminderBloc>().add(
ReminderEvent.markAsRead([reminder.id]),
);
}
},
);
},
),
);
}
}
class _InnerNotificationItem extends StatelessWidget {
const _InnerNotificationItem({
required this.reminder,
required this.tabType,
});
final MobileNotificationTabType tabType;
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const HSpace(8.0),
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
const HSpace(4.0),
NotificationIcon(reminder: reminder),
const HSpace(12.0),
Expanded(
child: NotificationContent(reminder: reminder),
),
],
);
}
}
class _SlidableNotificationItem extends StatelessWidget {
const _SlidableNotificationItem({
required this.tabType,
required this.reminder,
required this.child,
});
final MobileNotificationTabType tabType;
final ReminderPB reminder;
final Widget child;
@override
Widget build(BuildContext context) {
final List<NotificationPaneActionType> actions = switch (tabType) {
MobileNotificationTabType.inbox => [
NotificationPaneActionType.more,
if (!reminder.isRead) NotificationPaneActionType.markAsRead,
],
MobileNotificationTabType.unread => [
NotificationPaneActionType.more,
NotificationPaneActionType.markAsRead,
],
MobileNotificationTabType.archive => [
if (kDebugMode) NotificationPaneActionType.unArchive,
],
};
if (actions.isEmpty) {
return child;
}
final children = actions
.map(
(action) => action.actionButton(
context,
tabType: tabType,
),
)
.toList();
final extentRatio = actions.length == 1 ? 1 / 5 : 1 / 3;
return Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: extentRatio,
children: children,
),
child: child,
);
}
}
@@ -0,0 +1,167 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
enum _NotificationSettingsPopupMenuItem {
settings,
markAllAsRead,
archiveAll,
// only visible in debug mode
unarchiveAll;
}
class NotificationSettingsPopupMenu extends StatelessWidget {
const NotificationSettingsPopupMenu({super.key});
@override
Widget build(BuildContext context) {
return PopupMenuButton<_NotificationSettingsPopupMenuItem>(
offset: const Offset(0, 36),
padding: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12.0),
),
),
// todo: replace it with shadows
shadowColor: const Color(0x68000000),
elevation: 10,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: FlowySvg(
FlowySvgs.m_settings_more_s,
),
),
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<_NotificationSettingsPopupMenuItem>>[
_buildItem(
value: _NotificationSettingsPopupMenuItem.settings,
svg: FlowySvgs.m_notification_settings_s,
text: LocaleKeys.settings_notifications_settings_settings.tr(),
),
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _NotificationSettingsPopupMenuItem.markAllAsRead,
svg: FlowySvgs.m_notification_mark_as_read_s,
text: LocaleKeys.settings_notifications_settings_markAllAsRead.tr(),
),
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _NotificationSettingsPopupMenuItem.archiveAll,
svg: FlowySvgs.m_notification_archived_s,
text: LocaleKeys.settings_notifications_settings_archiveAll.tr(),
),
// only visible in debug mode
if (kDebugMode) ...[
const PopupMenuDivider(height: 0.5),
_buildItem(
value: _NotificationSettingsPopupMenuItem.unarchiveAll,
svg: FlowySvgs.m_notification_archived_s,
text: 'Unarchive all (Debug Mode)',
),
],
],
onSelected: (_NotificationSettingsPopupMenuItem value) {
switch (value) {
case _NotificationSettingsPopupMenuItem.markAllAsRead:
_onMarkAllAsRead(context);
break;
case _NotificationSettingsPopupMenuItem.archiveAll:
_onArchiveAll(context);
break;
case _NotificationSettingsPopupMenuItem.settings:
context.push(MobileHomeSettingPage.routeName);
break;
case _NotificationSettingsPopupMenuItem.unarchiveAll:
_onUnarchiveAll(context);
break;
}
},
);
}
PopupMenuItem<T> _buildItem<T>({
required T value,
required FlowySvgData svg,
required String text,
}) {
return PopupMenuItem<T>(
value: value,
padding: EdgeInsets.zero,
child: _PopupButton(
svg: svg,
text: text,
),
);
}
void _onMarkAllAsRead(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_allSuccess
.tr(),
);
context.read<ReminderBloc>().add(const ReminderEvent.markAllRead());
}
void _onArchiveAll(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
.tr(),
);
context.read<ReminderBloc>().add(const ReminderEvent.archiveAll());
}
void _onUnarchiveAll(BuildContext context) {
if (!kDebugMode) {
return;
}
showToastNotification(
context,
message: 'Unarchive all success (Debug Mode)',
);
context.read<ReminderBloc>().add(const ReminderEvent.unarchiveAll());
}
}
class _PopupButton extends StatelessWidget {
const _PopupButton({
required this.svg,
required this.text,
});
final FlowySvgData svg;
final String text;
@override
Widget build(BuildContext context) {
return Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
FlowySvg(svg),
const HSpace(12),
FlowyText.regular(
text,
fontSize: 16,
),
],
),
);
}
}
@@ -0,0 +1,286 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/color.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
const _kNotificationIconHeight = 36.0;
class NotificationIcon extends StatelessWidget {
const NotificationIcon({
super.key,
required this.reminder,
});
final ReminderPB reminder;
@override
Widget build(BuildContext context) {
return const FlowySvg(
FlowySvgs.m_notification_reminder_s,
size: Size.square(_kNotificationIconHeight),
blendMode: null,
);
}
}
class NotificationCheckIcon extends StatelessWidget {
const NotificationCheckIcon({super.key, required this.isSelected});
final bool isSelected;
@override
Widget build(BuildContext context) {
return SizedBox(
height: _kNotificationIconHeight,
child: Center(
child: FlowySvg(
isSelected
? FlowySvgs.m_notification_multi_select_s
: FlowySvgs.m_notification_multi_unselect_s,
blendMode: isSelected ? null : BlendMode.srcIn,
),
),
);
}
}
class UnreadRedDot extends StatelessWidget {
const UnreadRedDot({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
height: _kNotificationIconHeight,
child: Center(
child: SizedBox.square(
dimension: 6.0,
child: DecoratedBox(
decoration: ShapeDecoration(
color: Color(0xFFFF6331),
shape: OvalBorder(),
),
),
),
),
);
}
}
class NotificationContent extends StatefulWidget {
const NotificationContent({
super.key,
required this.reminder,
});
final ReminderPB reminder;
@override
State<NotificationContent> createState() => _NotificationContentState();
}
class _NotificationContentState extends State<NotificationContent> {
@override
void didUpdateWidget(covariant NotificationContent oldWidget) {
super.didUpdateWidget(oldWidget);
context.read<NotificationReminderBloc>().add(
const NotificationReminderEvent.reset(),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<NotificationReminderBloc, NotificationReminderState>(
builder: (context, state) {
final view = state.view;
if (view == null) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// title
_buildHeader(),
// time & page name
_buildTimeAndPageName(
context,
state.createdAt,
state.pageTitle,
),
// content
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: _buildContent(view, nodes: state.nodes),
),
],
);
},
);
}
Widget _buildContent(ViewPB view, {List<Node>? nodes}) {
if (view.layout.isDocumentView && nodes != null) {
return IntrinsicHeight(
child: BlocProvider(
create: (context) => DocumentPageStyleBloc(view: view),
child: NotificationDocumentContent(
reminder: widget.reminder,
nodes: nodes,
),
),
);
} else if (view.layout.isDatabaseView) {
final opacity = widget.reminder.type == ReminderType.past ? 0.3 : 1.0;
return Opacity(
opacity: opacity,
child: FlowyText(
widget.reminder.message,
fontSize: 14,
figmaLineHeight: 22,
color: context.notificationItemTextColor,
),
);
}
return const SizedBox.shrink();
}
Widget _buildHeader() {
return FlowyText.semibold(
LocaleKeys.settings_notifications_titles_reminder.tr(),
fontSize: 14,
figmaLineHeight: 20,
);
}
Widget _buildTimeAndPageName(
BuildContext context,
String createdAt,
String pageTitle,
) {
return Opacity(
opacity: 0.5,
child: Row(
children: [
// the legacy reminder doesn't contain the timestamp, so we don't show it
if (createdAt.isNotEmpty) ...[
FlowyText.regular(
createdAt,
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
),
const NotificationEllipse(),
],
FlowyText.regular(
pageTitle,
fontSize: 12,
figmaLineHeight: 18,
color: context.notificationItemTextColor,
),
],
),
);
}
}
class NotificationEllipse extends StatelessWidget {
const NotificationEllipse({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 2.50,
height: 2.50,
margin: const EdgeInsets.symmetric(horizontal: 5.0),
decoration: ShapeDecoration(
color: context.notificationItemTextColor,
shape: const OvalBorder(),
),
);
}
}
class NotificationDocumentContent extends StatelessWidget {
const NotificationDocumentContent({
super.key,
required this.reminder,
required this.nodes,
});
final ReminderPB reminder;
final List<Node> nodes;
@override
Widget build(BuildContext context) {
final editorState = EditorState(
document: Document(
root: pageNode(children: nodes),
),
);
final styleCustomizer = EditorStyleCustomizer(
context: context,
padding: EdgeInsets.zero,
);
final editorStyle = styleCustomizer.style().copyWith(
// hide the cursor
cursorColor: Colors.transparent,
cursorWidth: 0,
textStyleConfiguration: TextStyleConfiguration(
lineHeight: 22 / 14,
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
text: TextStyle(
fontSize: 14,
color: context.notificationItemTextColor,
height: 22 / 14,
fontWeight: FontWeight.w400,
leadingDistribution: TextLeadingDistribution.even,
),
),
);
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
customHeadingPadding: EdgeInsets.zero,
);
return IgnorePointer(
child: Opacity(
opacity: reminder.type == ReminderType.past ? 0.3 : 1,
child: AppFlowyEditor(
editorState: editorState,
editorStyle: editorStyle,
disableSelectionService: true,
disableKeyboardService: true,
disableScrollService: true,
editable: false,
shrinkWrap: true,
blockComponentBuilders: blockBuilders,
),
),
);
}
}
@@ -0,0 +1,212 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum NotificationPaneActionType {
more,
markAsRead,
// only used in the debug mode.
unArchive;
MobileSlideActionButton actionButton(
BuildContext context, {
required MobileNotificationTabType tabType,
}) {
switch (this) {
case NotificationPaneActionType.markAsRead:
return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.m_notification_action_mark_as_read_s,
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys
.settings_notifications_markAsReadNotifications_success
.tr(),
);
context.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isRead: true,
),
),
);
},
);
// this action is only used in the debug mode.
case NotificationPaneActionType.unArchive:
return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.m_notification_action_mark_as_read_s,
size: 24.0,
onPressed: (context) {
showToastNotification(
context,
message: 'Unarchive notification success',
);
context.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isArchived: false,
),
),
);
},
);
case NotificationPaneActionType.more:
return MobileSlideActionButton(
backgroundColor: const Color(0xE5515563),
svg: FlowySvgs.three_dots_s,
size: 24.0,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
onPressed: (context) {
final reminderBloc = context.read<ReminderBloc>();
final notificationReminderBloc =
context.read<NotificationReminderBloc>();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: reminderBloc),
BlocProvider.value(value: notificationReminderBloc),
],
child: _NotificationMoreActions(
onClickMultipleChoice: () {
Future.delayed(const Duration(milliseconds: 250), () {
bottomNavigationBarType.value =
BottomNavigationBarActionType
.notificationMultiSelect;
});
},
),
);
},
);
},
);
}
}
}
class _NotificationMoreActions extends StatelessWidget {
const _NotificationMoreActions({
required this.onClickMultipleChoice,
});
final VoidCallback onClickMultipleChoice;
@override
Widget build(BuildContext context) {
final reminder = context.read<NotificationReminderBloc>().reminder;
return Column(
children: [
if (!reminder.isRead)
FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.settings_notifications_action_markAsRead.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_notification_action_mark_as_read_s,
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => _onMarkAsRead(context),
),
FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.settings_notifications_action_multipleChoice.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_notification_action_multiple_choice_s,
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => _onMultipleChoice(context),
),
if (!reminder.isArchived)
FlowyOptionTile.text(
height: 52.0,
text: LocaleKeys.settings_notifications_action_archive.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_notification_action_archive_s,
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => _onArchive(context),
),
],
);
}
void _onMarkAsRead(BuildContext context) {
Navigator.of(context).pop();
showToastNotification(
context,
message: LocaleKeys.settings_notifications_markAsReadNotifications_success
.tr(),
);
context.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isRead: true,
),
),
);
}
void _onMultipleChoice(BuildContext context) {
Navigator.of(context).pop();
onClickMultipleChoice();
}
void _onArchive(BuildContext context) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_archiveNotifications_success
.tr()
.tr(),
);
context.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(
id: context.read<NotificationReminderBloc>().reminder.id,
isRead: true,
isArchived: true,
),
),
);
Navigator.of(context).pop();
}
}
@@ -0,0 +1,137 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class NotificationTab extends StatefulWidget {
const NotificationTab({
super.key,
required this.tabType,
});
final MobileNotificationTabType tabType;
@override
State<NotificationTab> createState() => _NotificationTabState();
}
class _NotificationTabState extends State<NotificationTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
final reminders = _filterReminders(state.reminders);
if (reminders.isEmpty) {
// add refresh indicator to the empty notification.
return EmptyNotification(
type: widget.tabType,
);
}
final child = ListView.separated(
itemCount: reminders.length,
separatorBuilder: (context, index) => const VSpace(8.0),
itemBuilder: (context, index) {
final reminder = reminders[index];
return NotificationItem(
key: ValueKey('${widget.tabType}_${reminder.id}'),
tabType: widget.tabType,
reminder: reminder,
);
},
);
return RefreshIndicator.adaptive(
onRefresh: () async => _onRefresh(context),
child: child,
);
},
);
}
Future<void> _onRefresh(BuildContext context) async {
context.read<ReminderBloc>().add(const ReminderEvent.refresh());
// at least 0.5 seconds to dismiss the refresh indicator.
// otherwise, it will be dismissed immediately.
await context.read<ReminderBloc>().stream.firstOrNull;
await Future.delayed(const Duration(milliseconds: 500));
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.settings_notifications_refreshSuccess.tr(),
);
}
}
List<ReminderPB> _filterReminders(List<ReminderPB> reminders) {
switch (widget.tabType) {
case MobileNotificationTabType.inbox:
return reminders.reversed
.where((reminder) => !reminder.isArchived)
.toList()
.unique((reminder) => reminder.id);
case MobileNotificationTabType.archive:
return reminders.reversed
.where((reminder) => reminder.isArchived)
.toList()
.unique((reminder) => reminder.id);
case MobileNotificationTabType.unread:
return reminders.reversed
.where((reminder) => !reminder.isRead)
.toList()
.unique((reminder) => reminder.id);
}
}
}
class MultiSelectNotificationTab extends StatelessWidget {
const MultiSelectNotificationTab({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
// find the reminders that are not archived or read.
final reminders = state.reminders.reversed
.where((reminder) => !reminder.isArchived || !reminder.isRead)
.toList();
if (reminders.isEmpty) {
// add refresh indicator to the empty notification.
return const SizedBox.shrink();
}
return ListView.separated(
itemCount: reminders.length,
separatorBuilder: (context, index) => const VSpace(8.0),
itemBuilder: (context, index) {
final reminder = reminders[index];
return MultiSelectNotificationItem(
key: ValueKey(reminder.id),
reminder: reminder,
);
},
);
},
);
}
}
@@ -0,0 +1,87 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:reorderable_tabbar/reorderable_tabbar.dart';
enum MobileNotificationTabType {
inbox,
unread,
archive;
String get tr {
switch (this) {
case MobileNotificationTabType.inbox:
return LocaleKeys.settings_notifications_tabs_inbox.tr();
case MobileNotificationTabType.unread:
return LocaleKeys.settings_notifications_tabs_unread.tr();
case MobileNotificationTabType.archive:
return LocaleKeys.settings_notifications_tabs_archived.tr();
}
}
List<NotificationPaneActionType> get actions {
switch (this) {
case MobileNotificationTabType.inbox:
return [
NotificationPaneActionType.more,
NotificationPaneActionType.markAsRead,
];
case MobileNotificationTabType.unread:
case MobileNotificationTabType.archive:
return [];
}
}
}
class MobileNotificationTabBar extends StatelessWidget {
const MobileNotificationTabBar({
super.key,
this.height = 38.0,
required this.tabController,
required this.tabs,
});
final double height;
final List<MobileNotificationTabType> tabs;
final TabController tabController;
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
final labelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 16.0,
height: 22.0 / 16.0,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 15.0,
height: 22.0 / 15.0,
);
return Container(
height: height,
padding: const EdgeInsets.only(left: 8.0),
child: ReorderableTabBar(
controller: tabController,
tabs: tabs.map((e) => Tab(text: e.tr)).toList(),
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
labelStyle: labelStyle,
labelColor: baseStyle?.color,
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
unselectedLabelStyle: unselectedLabelStyle,
overlayColor: WidgetStateProperty.all(Colors.transparent),
indicator: const RoundUnderlineTabIndicator(
width: 28.0,
borderSide: BorderSide(
color: Color(0xFF00C8FF),
width: 3,
),
),
),
);
}
}
@@ -0,0 +1,9 @@
export 'empty.dart';
export 'header.dart';
export 'multi_select_notification_item.dart';
export 'notification_item.dart';
export 'settings_popup_menu.dart';
export 'shared.dart';
export 'slide_actions.dart';
export 'tab.dart';
export 'tab_bar.dart';
Loaded 100 of 2610 files, more files were not shown because too many files have changed in this diff. Show more