Compare commits

..
Author SHA1 Message Date
Zack Fu Zi Xiang 12198dddb7 chore: update dotenv 2024-07-26 16:42:05 +08:00
Zack Fu Zi Xiang 5efdd6e8b9 chore: update to latest client api 2024-07-26 14:06:18 +08:00
Zack Fu Zi Xiang d1c867d089 chore: update client api 2024-07-26 13:46:06 +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
1106 changed files with 40636 additions and 25847 deletions

No files matched your search

-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
+49
View File
@@ -1,4 +1,53 @@
# Release Notes
## 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.6.0"
APPFLOWY_VERSION = "0.6.6"
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"
@@ -53,7 +53,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 24
targetSdkVersion 33
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -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);
});
}
@@ -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,11 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -116,6 +117,53 @@ 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 image(png) from memory', (tester) async {
@@ -210,42 +258,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 +295,7 @@ extension on WidgetTester {
Future<void> Function(EditorState editorState)? beforeTest,
String? plainText,
String? html,
String? inAppJson,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
@@ -271,6 +311,7 @@ extension on WidgetTester {
ClipboardServiceData(
plainText: plainText,
html: html,
inAppJson: inAppJson,
image: image,
),
);
@@ -16,6 +16,8 @@ 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 +40,7 @@ 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();
}
@@ -1,21 +1,22 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -36,7 +37,7 @@ 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
@@ -84,7 +85,7 @@ 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
@@ -137,7 +138,7 @@ 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
@@ -161,5 +162,67 @@ 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('Image');
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()]);
});
});
}
@@ -0,0 +1,287 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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/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/multi_image_layouts.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_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('Photo gallery');
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('Photo gallery');
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));
});
});
}
@@ -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,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_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';
@@ -103,8 +103,8 @@ Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
await getIt.unregister<AIRepository>();
getIt.registerFactoryAsync<AIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
@@ -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,7 +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';
@@ -37,8 +36,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);
@@ -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 {
@@ -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());
}
@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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';
@@ -10,12 +13,10 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl
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_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -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,
@@ -12,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
@@ -31,6 +32,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;
+6 -36
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
@@ -206,14 +181,11 @@ SPEC CHECKSUMS:
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 =
@@ -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),
),
),
),
),
);
}
+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();
}
@@ -125,7 +125,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
return child;
},
)
: child;
: SafeArea(child: child);
return Scaffold(
extendBodyBehindAppBar: isDocument,
appBar: appBar,
@@ -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,7 +1,7 @@
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';
@@ -23,7 +23,7 @@ enum MobilePaneActionType {
MobileSlideActionButton actionButton(
BuildContext context, {
MobileViewCardType? cardType,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
}) {
switch (this) {
@@ -133,13 +133,13 @@ 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
@@ -150,7 +150,7 @@ enum MobilePaneActionType {
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
case MobileViewCardType.favorite:
case MobilePageCardType.favorite:
return [
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
@@ -179,7 +179,7 @@ ActionPane buildEndActionPane(
BuildContext context,
List<MobilePaneActionType> actions, {
bool needSpace = true,
MobileViewCardType? cardType,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
}) {
return ActionPane(
@@ -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,7 +29,7 @@ const mobileSupportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.Time,
// FieldType.Time,
];
Future<FieldType?> showFieldTypeGridBottomSheet(
@@ -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,17 @@ 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(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
itemBuilder: (context, index) {
final view = favoriteViews[index];
@@ -105,16 +112,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,
),
);
},
@@ -28,9 +28,12 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: HomeSpaceViewSizes.mVerticalPadding,
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
top: HomeSpaceViewSizes.mVerticalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
child: MobileFolders(
user: widget.userProfile,
@@ -42,8 +42,8 @@ class MobileFolders extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) =>
SpaceBloc()..add(SpaceEvent.initial(user, workspaceId)),
create: (_) => SpaceBloc()
..add(SpaceEvent.initial(user, workspaceId, openFirstPage: false)),
),
],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
@@ -1,10 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
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/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@@ -15,7 +18,6 @@ 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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
@@ -57,6 +59,7 @@ class MobileHomeScreen extends StatelessWidget {
return Scaffold(
body: SafeArea(
bottom: false,
child: Provider.value(
value: userProfile,
child: MobileHomePage(
@@ -111,6 +114,9 @@ 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) =>
@@ -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,18 @@ 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(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
itemBuilder: (context, index) {
final sectionView = recentViews[index];
@@ -77,16 +84,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/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(),
};
}
@@ -10,6 +10,10 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
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/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_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@@ -23,19 +27,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,
@@ -44,7 +49,7 @@ class MobileViewCard extends StatelessWidget {
final ViewPB view;
final Int64? timestamp;
final MobileViewCardType type;
final MobilePageCardType type;
@override
Widget build(BuildContext context) {
@@ -153,6 +158,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,
@@ -185,6 +191,7 @@ class MobileViewCard extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w600,
height: 1.3,
),
),
],
@@ -203,40 +210,45 @@ class MobileViewCard extends StatelessWidget {
}
Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode
? const Color(0xFF171717)
: 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,20 +261,20 @@ class MobileViewCard extends StatelessWidget {
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;
@@ -273,6 +285,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) {
@@ -4,6 +4,7 @@ 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/mobile/presentation/presentation.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';
@@ -22,6 +23,18 @@ class MobileSpace extends StatefulWidget {
}
class _MobileSpaceState extends State<MobileSpace> {
@override
void initState() {
super.initState();
createNewPageNotifier.addListener(_createNewPage);
}
@override
void dispose() {
createNewPageNotifier.removeListener(_createNewPage);
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
@@ -41,11 +54,8 @@ class _MobileSpaceState extends State<MobileSpace> {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
index: 0,
viewSection: currentSpace.spacePermission ==
SpacePermission.publicToAll
? ViewSectionPB.Public
: ViewSectionPB.Private,
),
);
context.read<SpaceBloc>().add(
@@ -85,6 +95,15 @@ class _MobileSpaceState extends State<MobileSpace> {
},
);
}
void _createNewPage() {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
),
);
}
}
class _Pages extends StatelessWidget {
@@ -21,8 +21,8 @@ class MobileSpaceTabBar extends StatelessWidget {
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
final labelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15.0,
fontWeight: FontWeight.w600,
fontSize: 16.0,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
@@ -1,15 +1,15 @@
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/util/theme_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
PropertyValueNotifier(null);
const _homeLabel = 'home';
const _addLabel = 'add';
const _notificationLabel = 'notification';
@@ -46,22 +46,40 @@ class MobileBottomNavigationBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).isLightMode;
final backgroundColor = isLightMode
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
return Scaffold(
body: navigationShell,
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: _items,
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
extendBody: true,
bottomNavigationBar: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3,
sigmaY: 3,
),
child: DecoratedBox(
decoration: BoxDecoration(
border: isLightMode
? Border(
top: BorderSide(color: Theme.of(context).dividerColor),
)
: null,
color: 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),
),
),
),
),
);
@@ -72,7 +90,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
void _onTap(BuildContext context, int bottomBarIndex) {
if (_items[bottomBarIndex].label == _addLabel) {
// show an add dialog
_showCreatePageBottomSheet(context);
createNewPageNotifier.value = ViewLayoutPB.Document;
return;
}
// When navigating to a new branch, it's recommended to use the goBranch
@@ -87,40 +105,4 @@ 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');
});
},
);
},
);
}
}
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
),
if (kDebugMode)
MobileSettingItem(
@@ -17,17 +17,23 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
required this.questionId,
}) : super(ChatAIMessageState.initial(message)) {
if (state.stream != null) {
_subscription = state.stream!.listen((text) {
if (isClosed) {
return;
}
if (text.startsWith("data:")) {
add(ChatAIMessageEvent.newText(text.substring(5)));
} else if (text.startsWith("error:")) {
add(ChatAIMessageEvent.receiveError(text.substring(5)));
}
});
state.stream!.listen(
onData: (text) {
if (!isClosed) {
add(ChatAIMessageEvent.updateText(text));
}
},
onError: (error) {
if (!isClosed) {
add(ChatAIMessageEvent.receiveError(error.toString()));
}
},
onAIResponseLimit: () {
if (!isClosed) {
add(const ChatAIMessageEvent.onAIResponseLimit());
}
},
);
if (state.stream!.error != null) {
Future.delayed(const Duration(milliseconds: 300), () {
@@ -42,11 +48,16 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
(event, emit) async {
await event.when(
initial: () async {},
newText: (newText) {
emit(state.copyWith(text: state.text + newText, error: null));
updateText: (newText) {
emit(
state.copyWith(
text: newText,
messageState: const MessageState.ready(),
),
);
},
receiveError: (error) {
emit(state.copyWith(error: error));
emit(state.copyWith(messageState: MessageState.onError(error)));
},
retry: () {
if (questionId is! Int64) {
@@ -55,8 +66,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
}
emit(
state.copyWith(
retryState: const LoadingState.loading(),
error: null,
messageState: const MessageState.loading(),
),
);
@@ -82,8 +92,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
emit(
state.copyWith(
text: text,
error: null,
retryState: const LoadingState.finish(),
messageState: const MessageState.ready(),
),
);
},
onAIResponseLimit: () {
emit(
state.copyWith(
messageState: const MessageState.onAIResponseLimit(),
),
);
},
@@ -92,13 +108,6 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
);
}
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
StreamSubscription<AnswerStreamElement>? _subscription;
final String chatId;
final Int64? questionId;
}
@@ -106,26 +115,34 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
@freezed
class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.initial() = Initial;
const factory ChatAIMessageEvent.newText(String text) = _NewText;
const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
const factory ChatAIMessageEvent.retry() = _Retry;
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
}
@freezed
class ChatAIMessageState with _$ChatAIMessageState {
const factory ChatAIMessageState({
AnswerStream? stream,
String? error,
required String text,
required LoadingState retryState,
required MessageState messageState,
}) = _ChatAIMessageState;
factory ChatAIMessageState.initial(dynamic text) {
return ChatAIMessageState(
text: text is String ? text : "",
stream: text is AnswerStream ? text : null,
retryState: const LoadingState.finish(),
messageState: const MessageState.ready(),
);
}
}
@freezed
class MessageState with _$MessageState {
const factory MessageState.onError(String error) = _Error;
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
const factory MessageState.ready() = _Ready;
const factory MessageState.loading() = _Loading;
}
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@@ -16,7 +17,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/nanoid.dart';
import 'chat_message_listener.dart';
part 'chat_bloc.freezed.dart';
const sendMessageErrorKey = "sendMessageError";
@@ -67,7 +70,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
chatId: state.view.id,
limit: Int64(10),
);
ChatEventLoadNextMessage(payload).send();
ChatEventLoadNextMessage(payload).send().then(
(result) {
result.fold((list) {
if (!isClosed) {
final messages =
list.messages.map(_createTextMessage).toList();
add(ChatEvent.didLoadLatestMessages(messages));
}
}, (err) {
Log.error("Failed to load messages: $err");
});
},
);
},
startLoadingPrevMessage: () async {
Int64? beforeMessageId;
@@ -105,7 +120,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final uniqueMessages = {...allMessages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id));
uniqueMessages.insertAll(0, onetimeMessages);
emit(
state.copyWith(
messages: uniqueMessages,
@@ -366,7 +380,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
}
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
final streamMessageId = nanoid();
final streamMessageId = (questionMessageId + 1).toString();
lastStreamMessageId = streamMessageId;
return TextMessage(
@@ -377,6 +392,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
"chatId": chatId,
},
id: streamMessageId,
createdAt: DateTime.now().millisecondsSinceEpoch,
text: '',
);
}
@@ -393,7 +409,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
author: User(id: message.authorId),
id: messageId,
text: message.content,
createdAt: message.createdAt.toInt(),
createdAt: message.createdAt.toInt() * 1000,
);
}
}
@@ -465,7 +481,7 @@ class ChatState with _$ChatState {
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
const factory LoadingState.finish({FlowyError? error}) = _Finish;
}
enum OnetimeShotType {
@@ -509,8 +525,6 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
return null;
}
typedef AnswerStreamElement = String;
class AnswerStream {
AnswerStream() {
_port.handler = _controller.add;
@@ -518,23 +532,53 @@ class AnswerStream {
(event) {
if (event.startsWith("data:")) {
_hasStarted = true;
final newText = event.substring(5);
_text += newText;
if (_onData != null) {
_onData!(_text);
}
} else if (event.startsWith("error:")) {
_error = event.substring(5);
if (_onError != null) {
_onError!(_error!);
}
} else if (event == "AI_RESPONSE_LIMIT") {
if (_onAIResponseLimit != null) {
_onAIResponseLimit!();
}
}
},
onDone: () {
if (_onEnd != null) {
_onEnd!();
}
},
onError: (error) {
if (_onError != null) {
_onError!(error.toString());
}
},
);
}
final RawReceivePort _port = RawReceivePort();
final StreamController<AnswerStreamElement> _controller =
StreamController.broadcast();
late StreamSubscription<AnswerStreamElement> _subscription;
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
bool _hasStarted = false;
String? _error;
String _text = "";
// Callbacks
void Function(String text)? _onData;
void Function()? _onStart;
void Function()? _onEnd;
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
String? get error => _error;
String get text => _text;
Future<void> dispose() async {
await _controller.close();
@@ -542,9 +586,21 @@ class AnswerStream {
_port.close();
}
StreamSubscription<AnswerStreamElement> listen(
void Function(AnswerStreamElement event)? onData,
) {
return _controller.stream.listen(onData);
void listen({
void Function(String text)? onData,
void Function()? onStart,
void Function()? onEnd,
void Function(String error)? onError,
void Function()? onAIResponseLimit,
}) {
_onData = onData;
_onStart = onStart;
_onEnd = onEnd;
_onError = onError;
_onAIResponseLimit = onAIResponseLimit;
if (_onStart != null) {
_onStart!();
}
}
}
@@ -0,0 +1,139 @@
import 'dart:async';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_file_bloc.freezed.dart';
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
ChatFileBloc({
required String chatId,
}) : listener = LocalLLMListener(),
super(const ChatFileState()) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(ChatFileEvent.updatePluginState(pluginState));
}
},
chatStateCallback: (chatState) {
if (!isClosed) {
add(ChatFileEvent.updateChatState(chatState));
}
},
);
on<ChatFileEvent>(
(event, emit) async {
await event.when(
initial: () async {
final result = await ChatEventGetLocalAIChatState().send();
result.fold(
(chatState) {
if (!isClosed) {
add(
ChatFileEvent.updateChatState(chatState),
);
}
},
(err) {
Log.error(err.toString());
},
);
},
newFile: (String filePath, String fileName) async {
emit(
state.copyWith(
indexFileIndicator: IndexFileIndicator.indexing(fileName),
),
);
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
unawaited(
ChatEventChatWithFile(payload).send().then((result) {
if (!isClosed) {
result.fold((_) {
add(
ChatFileEvent.updateIndexFile(
IndexFileIndicator.finish(fileName),
),
);
}, (err) {
add(
ChatFileEvent.updateIndexFile(
IndexFileIndicator.error(err.msg),
),
);
});
}
}),
);
},
updateChatState: (LocalAIChatPB chatState) {
// Only user enable chat with file and the plugin is already running
final supportChatWithFile = chatState.fileEnabled &&
chatState.pluginState.state == RunningStatePB.Running;
emit(
state.copyWith(
supportChatWithFile: supportChatWithFile,
chatState: chatState,
),
);
},
updateIndexFile: (IndexFileIndicator indicator) {
emit(
state.copyWith(indexFileIndicator: indicator),
);
},
updatePluginState: (LocalAIPluginStatePB chatState) {
final fileEnabled = state.chatState?.fileEnabled ?? false;
final supportChatWithFile =
fileEnabled && chatState.state == RunningStatePB.Running;
emit(state.copyWith(supportChatWithFile: supportChatWithFile));
},
);
},
);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed
class ChatFileEvent with _$ChatFileEvent {
const factory ChatFileEvent.initial() = Initial;
const factory ChatFileEvent.newFile(String filePath, String fileName) =
_NewFile;
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
_UpdateChatState;
const factory ChatFileEvent.updatePluginState(
LocalAIPluginStatePB chatState,
) = _UpdatePluginState;
const factory ChatFileEvent.updateIndexFile(IndexFileIndicator indicator) =
_UpdateIndexFile;
}
@freezed
class ChatFileState with _$ChatFileState {
const factory ChatFileState({
@Default(false) bool supportChatWithFile,
IndexFileIndicator? indexFileIndicator,
LocalAIChatPB? chatState,
}) = _ChatFileState;
}
@freezed
class IndexFileIndicator with _$IndexFileIndicator {
const factory IndexFileIndicator.finish(String fileName) = _Finish;
const factory IndexFileIndicator.indexing(String fileName) = _Indexing;
const factory IndexFileIndicator.error(String error) = _Error;
}
@@ -0,0 +1,73 @@
import 'dart:async';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_input_bloc.freezed.dart';
class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
ChatInputBloc()
: listener = LocalLLMListener(),
super(const ChatInputState(aiType: _AppFlowyAI())) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(ChatInputEvent.updateState(pluginState));
}
},
);
on<ChatInputEvent>(_handleEvent);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
Future<void> _handleEvent(
ChatInputEvent event,
Emitter<ChatInputState> emit,
) async {
await event.when(
started: () async {
final result = await ChatEventGetLocalAIPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(ChatInputEvent.updateState(pluginState));
}
},
(err) => Log.error(err.toString()),
);
},
updateState: (LocalAIPluginStatePB aiPluginState) {
emit(const ChatInputState(aiType: _AppFlowyAI()));
},
);
}
}
@freezed
class ChatInputEvent with _$ChatInputEvent {
const factory ChatInputEvent.started() = _Started;
const factory ChatInputEvent.updateState(LocalAIPluginStatePB aiPluginState) =
_UpdatePluginState;
}
@freezed
class ChatInputState with _$ChatInputState {
const factory ChatInputState({required AIType aiType}) = _ChatInputState;
}
@freezed
class AIType with _$AIType {
const factory AIType.appflowyAI() = _AppFlowyAI;
const factory AIType.localAI() = _LocalAI;
}
@@ -1,5 +1,7 @@
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -13,9 +15,23 @@ class ChatUserMessageBloc
}) : super(ChatUserMessageState.initial(message)) {
on<ChatUserMessageEvent>(
(event, emit) async {
await event.when(
initial: () async {},
update: (userProfile, deviceId, states) {},
event.when(
initial: () {
final payload =
WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id));
UserEventGetMemberInfo(payload).send().then((result) {
if (!isClosed) {
result.fold((member) {
add(ChatUserMessageEvent.didReceiveMemberInfo(member));
}, (err) {
Log.error("Error getting member info: $err");
});
}
});
},
didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) {
emit(state.copyWith(member: memberInfo));
},
);
},
);
@@ -25,11 +41,9 @@ class ChatUserMessageBloc
@freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.update(
UserProfilePB userProfile,
String deviceId,
DocumentAwarenessStatesPB states,
) = Update;
const factory ChatUserMessageEvent.didReceiveMemberInfo(
WorkspaceMemberPB memberInfo,
) = _MemberInfo;
}
@freezed
@@ -65,6 +65,12 @@ class AIChatPagePlugin extends Plugin {
_viewInfoBloc = ViewInfoBloc(view: notifier.view)
..add(const ViewInfoEvent.started());
}
@override
void dispose() {
_viewInfoBloc.close();
notifier.dispose();
}
}
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
@@ -89,6 +95,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
Map<String, dynamic>? data,
}) {
notifier.isDeleted.addListener(() {
final deletedView = notifier.isDeleted.value;
@@ -111,4 +118,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
@override
List<NavigationItem> get navigationItems => [this];
@override
EdgeInsets get contentPadding => EdgeInsets.zero;
}
@@ -1,3 +1,11 @@
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
@@ -10,12 +18,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'presentation/chat_input.dart';
import 'presentation/chat_popmenu.dart';
@@ -27,11 +33,11 @@ import 'presentation/message/user_text_message.dart';
class AIChatUILayout {
static EdgeInsets get chatPadding =>
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 70);
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 20);
static EdgeInsets get welcomePagePadding => isMobile
? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 100);
: const EdgeInsets.symmetric(horizontal: 50);
static double get messageWidthRatio => 0.85;
@@ -44,11 +50,12 @@ class AIChatUILayout {
query.padding.right,
query.viewInsets.bottom + query.padding.bottom,
)
: const EdgeInsets.symmetric(horizontal: 70);
: const EdgeInsets.symmetric(horizontal: 50) +
const EdgeInsets.only(bottom: 20);
}
}
class AIChatPage extends StatefulWidget {
class AIChatPage extends StatelessWidget {
const AIChatPage({
super.key,
required this.view,
@@ -61,10 +68,112 @@ class AIChatPage extends StatefulWidget {
final UserProfilePB userProfile;
@override
State<AIChatPage> createState() => _AIChatPageState();
Widget build(BuildContext context) {
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => ChatFileBloc(chatId: view.id.toString())
..add(const ChatFileEvent.initial()),
),
BlocProvider(
create: (_) => ChatBloc(
view: view,
userProfile: userProfile,
)..add(const ChatEvent.initialLoad()),
),
BlocProvider(create: (_) => ChatInputBloc()),
],
child: BlocListener<ChatFileBloc, ChatFileState>(
listenWhen: (previous, current) =>
previous.indexFileIndicator != current.indexFileIndicator,
listener: (context, state) {
_handleIndexIndicator(state.indexFileIndicator, context);
},
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return DropTarget(
onDragDone: (DropDoneDetails detail) async {
if (state.supportChatWithFile) {
await showConfirmDialog(
context: context,
style: ConfirmPopupStyle.cancelAndOk,
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path, file.name));
}
},
description: '',
);
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
);
},
),
),
);
}
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
void _handleIndexIndicator(
IndexFileIndicator? indicator,
BuildContext context,
) {
if (indicator != null) {
indicator.when(
finish: (fileName) {
showSnackBarMessage(
context,
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
);
},
indexing: (fileName) {
showSnackBarMessage(
context,
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
duration: const Duration(seconds: 2),
);
},
error: (err) {
showSnackBarMessage(
context,
err,
);
},
);
}
}
}
class _AIChatPageState extends State<AIChatPage> {
class _ChatContentPage extends StatefulWidget {
const _ChatContentPage({
required this.view,
required this.userProfile,
});
final UserProfilePB userProfile;
final ViewPB view;
@override
State<_ChatContentPage> createState() => _ChatContentPageState();
}
class _ChatContentPageState extends State<_ChatContentPage> {
late types.User _user;
@override
@@ -77,31 +186,27 @@ class _AIChatPageState extends State<AIChatPage> {
Widget build(BuildContext context) {
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
return buildChatWidget();
} else {
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
Widget buildChatWidget() {
return SizedBox.expand(
child: Padding(
padding: AIChatUILayout.chatPadding,
child: BlocProvider(
create: (context) => ChatBloc(
view: widget.view,
userProfile: widget.userProfile,
)..add(const ChatEvent.initialLoad()),
child: BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) {
return Chat(
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 784),
child: BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) => Chat(
messages: state.messages,
onAttachmentPressed: () {},
onSendPressed: (types.PartialText message) {
onSendPressed: (_) {
// We use custom bottom widget for chat input, so
// do not need to handle this event.
},
@@ -118,32 +223,27 @@ class _AIChatPageState extends State<AIChatPage> {
}
},
emptyState: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.initialLoadingStatus ==
const LoadingState.finish()
? Padding(
padding: AIChatUILayout.welcomePagePadding,
child: ChatWelcomePage(
onSelectedQuestion: (question) {
blocContext
builder: (_, state) =>
state.initialLoadingStatus == const LoadingState.finish()
? Padding(
padding: AIChatUILayout.welcomePagePadding,
child: ChatWelcomePage(
onSelectedQuestion: (question) => blocContext
.read<ChatBloc>()
.add(ChatEvent.sendMessage(question));
},
.add(ChatEvent.sendMessage(question)),
),
)
: const Center(
child: CircularProgressIndicator.adaptive(),
),
)
: const Center(
child: CircularProgressIndicator.adaptive(),
);
},
),
messageWidthRatio: AIChatUILayout.messageWidthRatio,
textMessageBuilder: (
textMessage, {
required messageWidth,
required showName,
}) {
return _buildAITextMessage(blocContext, textMessage);
},
}) =>
_buildAITextMessage(blocContext, textMessage),
bubbleBuilder: (
child, {
required message,
@@ -154,15 +254,15 @@ class _AIChatPageState extends State<AIChatPage> {
message: message,
child: child,
);
} else {
return _buildAIBubble(message, blocContext, state, child);
}
return _buildAIBubble(message, blocContext, state, child);
},
);
},
),
),
),
),
),
],
);
}
@@ -289,31 +389,40 @@ class _AIChatPageState extends State<AIChatPage> {
return ClipRect(
child: Padding(
padding: AIChatUILayout.safeAreaInsets(context),
child: Column(
children: [
BlocSelector<ChatBloc, ChatState, LoadingState>(
selector: (state) => state.streamingStatus,
builder: (context, state) {
return ChatInput(
chatId: widget.view.id,
onSendPressed: (message) =>
onSendPressed(context, message.text),
isStreaming: state != const LoadingState.finish(),
onStopStreaming: () {
context.read<ChatBloc>().add(const ChatEvent.stopStream());
},
);
},
),
const VSpace(6),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
child: BlocBuilder<ChatInputBloc, ChatInputState>(
builder: (context, state) {
return state.aiType.when(
appflowyAI: () => Column(
children: [
BlocSelector<ChatBloc, ChatState, LoadingState>(
selector: (state) => state.streamingStatus,
builder: (context, state) {
return ChatInput(
chatId: widget.view.id,
onSendPressed: (message) =>
onSendPressed(context, message.text),
isStreaming: state != const LoadingState.finish(),
onStopStreaming: () {
context
.read<ChatBloc>()
.add(const ChatEvent.stopStream());
},
);
},
),
const VSpace(6),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
),
),
],
),
),
],
localAI: () => const SizedBox.shrink(),
);
},
),
),
);
@@ -1,10 +1,16 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget {
size: const Size.square(14),
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: textMessage.text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
onPressed: () async {
final document = customMarkdownToDocument(textMessage.text);
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: textMessage.text,
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
},
),
);
@@ -49,7 +49,9 @@ class _ChatInputState extends State<ChatInput> {
return KeyEventResult.ignored;
}
if (event is KeyDownEvent) {
_handleSendPressed();
if (!widget.isStreaming) {
_handleSendPressed();
}
}
return KeyEventResult.handled;
} else {
@@ -65,8 +67,7 @@ class _ChatInputState extends State<ChatInput> {
void initState() {
super.initState();
_textController =
widget.options.textEditingController ?? InputTextFieldController();
_textController = InputTextFieldController();
_handleSendButtonVisibilityModeChange();
}
@@ -78,18 +79,12 @@ class _ChatInputState extends State<ChatInput> {
}
void _handleSendPressed() {
if (widget.isStreaming) {
widget.onStopStreaming();
} else {
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
if (widget.options.inputClearMode == InputClearMode.always) {
_textController.clear();
}
}
_textController.clear();
}
}
@@ -108,7 +103,6 @@ class _ChatInputState extends State<ChatInput> {
const inputPadding = EdgeInsets.all(6);
return Focus(
autofocus: !widget.options.autofocus,
child: Padding(
padding: inputPadding,
child: Material(
@@ -139,7 +133,6 @@ class _ChatInputState extends State<ChatInput> {
padding: textPadding,
child: TextField(
controller: _textController,
readOnly: widget.isStreaming,
focusNode: _inputFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
@@ -151,16 +144,11 @@ class _ChatInputState extends State<ChatInput> {
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
),
enabled: widget.options.enabled,
autocorrect: widget.options.autocorrect,
autofocus: widget.options.autofocus,
enableSuggestions: widget.options.enableSuggestions,
keyboardType: widget.options.keyboardType,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
maxLines: 10,
minLines: 1,
onChanged: widget.options.onTextChanged,
onTap: widget.options.onTextFieldTap,
onChanged: (_) {},
),
);
}
@@ -176,7 +164,10 @@ class _ChatInputState extends State<ChatInput> {
padding: buttonPadding,
child: AccessoryButton(
onSendPressed: () {
_handleSendPressed();
if (!widget.isStreaming) {
widget.onStopStreaming();
_handleSendPressed();
}
},
onStopStreaming: () {
widget.onStopStreaming();
@@ -208,53 +199,6 @@ class _ChatInputState extends State<ChatInput> {
);
}
@immutable
class InputOptions {
const InputOptions({
this.inputClearMode = InputClearMode.always,
this.keyboardType = TextInputType.multiline,
this.onTextChanged,
this.onTextFieldTap,
this.textEditingController,
this.autocorrect = true,
this.autofocus = false,
this.enableSuggestions = true,
this.enabled = true,
});
/// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always].
final InputClearMode inputClearMode;
/// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline].
final TextInputType keyboardType;
/// Will be called whenever the text inside [TextField] changes.
final void Function(String)? onTextChanged;
/// Will be called on [TextField] tap.
final VoidCallback? onTextFieldTap;
/// Custom [TextEditingController]. If not provided, defaults to the
/// [InputTextFieldController], which extends [TextEditingController] and has
/// additional fatures like markdown support. If you want to keep additional
/// features but still need some methods from the default [TextEditingController],
/// you can create your own [InputTextFieldController] (imported from this lib)
/// and pass it here.
final TextEditingController? textEditingController;
/// Controls the [TextInput] autocorrect behavior. Defaults to [true].
final bool autocorrect;
/// Whether [TextInput] should have focus. Defaults to [false].
final bool autofocus;
/// Controls the [TextInput] enableSuggestions behavior. Defaults to [true].
final bool enableSuggestions;
/// Controls the [TextInput] enabled behavior. Defaults to [true].
final bool enabled;
}
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS;
@@ -6,7 +6,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class RelatedQuestionList extends StatelessWidget {
const RelatedQuestionList({
required this.chatId,
@@ -97,6 +96,7 @@ class _RelatedQuestionItemState extends State<RelatedQuestionItem> {
style: TextStyle(
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
fontSize: 14,
height: 1.5,
),
),
onTap: () {
@@ -31,19 +31,15 @@ class ChatWelcomePage extends StatelessWidget {
size: Size.square(44),
),
const SizedBox(height: 40),
GridView.builder(
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 16.0 / 9.0,
),
itemCount: items.length,
itemBuilder: (context, index) => WelcomeQuestion(
question: items[index],
onSelected: onSelectedQuestion,
),
Wrap(
children: items
.map(
(i) => WelcomeQuestion(
question: i,
onSelected: onSelectedQuestion,
),
)
.toList(),
),
],
),
@@ -0,0 +1,375 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
enum AIMarkdownType {
appflowyEditor,
markdownWidget,
}
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
class AIMarkdownText extends StatelessWidget {
const AIMarkdownText({
super.key,
required this.markdown,
this.type = AIMarkdownType.appflowyEditor,
});
final String markdown;
final AIMarkdownType type;
@override
Widget build(BuildContext context) {
switch (type) {
case AIMarkdownType.appflowyEditor:
return _AppFlowyEditorMarkdown(markdown: markdown);
case AIMarkdownType.markdownWidget:
return _ThirdPartyMarkdown(markdown: markdown);
}
}
}
class _AppFlowyEditorMarkdown extends StatefulWidget {
const _AppFlowyEditorMarkdown({
required this.markdown,
});
// the text should be the markdown format
final String markdown;
@override
State<_AppFlowyEditorMarkdown> createState() =>
_AppFlowyEditorMarkdownState();
}
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
late EditorState editorState;
late EditorScrollController scrollController;
@override
void initState() {
super.initState();
editorState = _parseMarkdown(widget.markdown);
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
@override
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.markdown != widget.markdown) {
editorState.dispose();
editorState = _parseMarkdown(widget.markdown);
scrollController.dispose();
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
}
@override
void dispose() {
scrollController.dispose();
editorState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// don't lazy load the styleCustomizer and blockBuilders,
// it needs the context to get the theme.
final styleCustomizer = EditorStyleCustomizer(
context: context,
padding: EdgeInsets.zero,
);
final editorStyle = styleCustomizer.style().copyWith(
// hide the cursor
cursorColor: Colors.transparent,
cursorWidth: 0,
);
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
);
return IntrinsicHeight(
child: AppFlowyEditor(
shrinkWrap: true,
// the editor is not editable in the chat
editable: false,
editorStyle: editorStyle,
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
commandShortcutEvents: [customCopyCommand],
editorState: editorState,
),
);
}
EditorState _parseMarkdown(String markdown) {
final document = customMarkdownToDocument(markdown);
final editorState = EditorState(document: document);
return editorState;
}
}
class _ThirdPartyMarkdown extends StatelessWidget {
const _ThirdPartyMarkdown({
required this.markdown,
});
final String markdown;
@override
Widget build(BuildContext context) {
return MarkdownWidget(
data: markdown,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
_ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHighlightTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'quote': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
}
class _ChatH1Config extends HeadingConfig {
const _ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class _ChatH2Config extends HeadingConfig {
const _ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class _ChatH3Config extends HeadingConfig {
const _ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
@@ -1,17 +1,15 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.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/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:markdown_widget/markdown_widget.dart';
class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({
@@ -39,202 +37,38 @@ class ChatAITextMessageWidget extends StatelessWidget {
)..add(const ChatAIMessageEvent.initial()),
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
builder: (context, state) {
if (state.error != null) {
return StreamingError(
onRetryPressed: () {
context.read<ChatAIMessageBloc>().add(
const ChatAIMessageEvent.retry(),
);
},
);
}
if (state.retryState == const LoadingState.loading()) {
return const ChatAILoading();
}
if (state.text.isEmpty) {
return const ChatAILoading();
} else {
return _textWidgetBuilder(user, context, state.text);
}
return state.messageState.when(
onError: (err) {
return StreamingError(
onRetryPressed: () {
context.read<ChatAIMessageBloc>().add(
const ChatAIMessageEvent.retry(),
);
},
);
},
onAIResponseLimit: () {
return FlowyText(
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
maxLines: 10,
lineHeight: 1.5,
);
},
ready: () {
if (state.text.isEmpty) {
return const ChatAILoading();
} else {
return AIMarkdownText(markdown: state.text);
}
},
loading: () {
return const ChatAILoading();
},
);
},
),
);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return MarkdownWidget(
data: text,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(8.0),
),
),
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig.darkConfig,
],
);
}
}
class ChatH1Config extends HeadingConfig {
const ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class ChatH2Config extends HeadingConfig {
const ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class ChatH3Config extends HeadingConfig {
const ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class StreamingError extends StatelessWidget {
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:highlight/highlight.dart';
/// Highlight Flutter Widget
class SelectableHighlightView extends StatelessWidget {
SelectableHighlightView(
String input, {
super.key,
this.language,
this.theme = const {},
this.padding,
this.textStyle,
int tabSize = 8,
}) : source = input.replaceAll('\t', ' ' * tabSize);
/// The original code to be highlighted
final String source;
/// Highlight language
///
/// It is recommended to give it a value for performance
///
/// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages)
final String? language;
/// Highlight theme
///
/// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes)
final Map<String, TextStyle> theme;
/// Padding
final EdgeInsetsGeometry? padding;
/// Text styles
///
/// Specify text styles such as font family and font size
final TextStyle? textStyle;
List<TextSpan> _convert(List<Node> nodes) {
final List<TextSpan> spans = [];
var currentSpans = spans;
final List<List<TextSpan>> stack = [];
// ignore: always_declare_return_types
traverse(Node node) {
if (node.value != null) {
currentSpans.add(
node.className == null
? TextSpan(text: node.value)
: TextSpan(text: node.value, style: theme[node.className!]),
);
} else if (node.children != null) {
final List<TextSpan> tmp = [];
currentSpans
.add(TextSpan(children: tmp, style: theme[node.className!]));
stack.add(currentSpans);
currentSpans = tmp;
for (final n in node.children!) {
traverse(n);
if (n == node.children!.last) {
currentSpans = stack.isEmpty ? spans : stack.removeLast();
}
}
}
}
for (final node in nodes) {
traverse(node);
}
return spans;
}
static const _rootKey = 'root';
static const _defaultBackgroundColor = Color(0xffffffff);
@override
Widget build(BuildContext context) {
return Container(
color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor,
padding: padding,
child: SelectableText.rich(
TextSpan(
style: textStyle,
children:
_convert(highlight.parse(source, language: language).nodes!),
),
),
);
}
}
@@ -52,8 +52,8 @@ class TextMessageText extends StatelessWidget {
text,
fontSize: 16,
fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: 2000,
maxLines: null,
selectable: true,
color: AFThemeExtension.of(context).textColor,
);
}
@@ -29,7 +29,8 @@ class ChatUserMessageBubble extends StatelessWidget {
Theme.of(context).colorScheme.surfaceContainerHighest;
return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message),
create: (context) => ChatUserMessageBloc(message: message)
..add(const ChatUserMessageEvent.initial()),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
return Row(
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
// use a global value to store the selected emoji to prevent reloading every time.
@@ -37,9 +38,7 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
EmojiData.builtIn().then(
(value) {
kCachedEmojiData = value;
setState(() {
emojiData = value;
});
setState(() => emojiData = value);
},
);
}
@@ -54,6 +54,7 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
Map<String, dynamic>? data,
}) =>
const BlankPage();
@@ -1,111 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'tag_cell_bloc.freezed.dart';
class TagCellBloc extends Bloc<TagCellEvent, TagCellState> {
TagCellBloc({
required this.cellController,
}) : super(TagCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final TagCellController cellController;
void Function()? _onCellChangedFn;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
await cellController.dispose();
return super.close();
}
void _dispatch() {
on<TagCellEvent>(
(event, emit) async {
await event.when(
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(content: cellData ?? ""),
);
},
didUpdateField: (fieldInfo) {
final wrap = fieldInfo.wrapCellContent;
if (wrap != null) {
emit(state.copyWith(wrap: wrap));
}
},
updateCell: (text) async {
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
TagCellEvent.didReceiveCellUpdate(
cellController.getCellData() ?? "",
),
);
}
},
);
},
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(
TagCellEvent.didReceiveCellUpdate(cellContent ?? ""),
);
}
},
onFieldChanged: _onFieldChangedListener,
);
}
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(TagCellEvent.didUpdateField(fieldInfo));
}
}
}
@freezed
class TagCellEvent with _$TagCellEvent {
const factory TagCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
const factory TagCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
const factory TagCellEvent.updateCell(String text) = _UpdateCell;
}
@freezed
class TagCellState with _$TagCellState {
const factory TagCellState({
required String content,
required bool wrap,
}) = _TagCellState;
factory TagCellState.initial(TagCellController cellController) {
final wrap = cellController.fieldInfo.wrapCellContent;
return TagCellState(
content: cellController.getCellData() ?? "",
wrap: wrap ?? true,
);
}
}
@@ -18,7 +18,6 @@ typedef RelationCellController = CellController<RelationCellDataPB, String>;
typedef SummaryCellController = CellController<String, String>;
typedef TimeCellController = CellController<TimeCellDataPB, String>;
typedef TranslateCellController = CellController<String, String>;
typedef TagCellController = CellController<String, String>;
CellController makeCellController(
DatabaseController databaseController,
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/view/view_cache.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
@@ -12,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'defines.dart';
import 'row/row_cache.dart';
@@ -223,6 +224,7 @@ class DatabaseController {
_databaseCallbacks.clear();
_groupCallbacks.clear();
_layoutCallbacks.clear();
_isLoading.dispose();
}
Future<void> _loadGroups() async {
@@ -1,7 +1,5 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
@@ -19,9 +17,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import '../setting/setting_service.dart';
import 'field_info.dart';
class _GridFieldNotifier extends ChangeNotifier {
@@ -76,7 +74,6 @@ typedef OnReceiveFields = void Function(List<FieldInfo>);
typedef OnReceiveFilters = void Function(List<FilterInfo>);
typedef OnReceiveSorts = void Function(List<SortInfo>);
class FieldController {
FieldController({required this.viewId})
: _fieldListener = FieldsListener(viewId: viewId),
@@ -446,9 +443,13 @@ class FieldController {
/// Listen for field setting changes in the backend.
void _listenOnFieldSettingsChanged() {
FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
final List<FieldInfo> newFields = fieldInfos;
FieldInfo updatedField = newFields[0];
var updatedField = newFields.firstOrNull;
if (updatedField == null) {
return null;
}
final index = newFields
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
@@ -470,6 +471,10 @@ class FieldController {
result.fold(
(fieldSettings) {
final updatedFieldInfo = updateFieldSettings(fieldSettings);
if (updatedFieldInfo == null) {
return;
}
for (final listener in _updatedFieldCallbacks.values) {
listener([updatedFieldInfo]);
}
@@ -49,9 +49,7 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
on<DatabaseGroupEvent>(
(event, emit) async {
await event.when(
initial: () {
_startListening();
},
initial: () async => _startListening(),
didReceiveFieldUpdate: (fieldInfos) {
emit(
state.copyWith(
@@ -52,7 +52,7 @@ class DatabaseTabBarBloc
_createLinkedView(layout.layoutType, name ?? layout.layoutName);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
final result = await ViewBackendService.deleteView(viewId: viewId);
result.fold(
(l) {},
(r) => Log.error(r),
@@ -464,7 +464,7 @@ class _BoardColumnFooterState extends State<BoardColumnFooter> {
const SingleActivator(LogicalKeyboardKey.keyE): nada,
const SingleActivator(LogicalKeyboardKey.keyN): nada,
const SingleActivator(LogicalKeyboardKey.delete): nada,
const SingleActivator(LogicalKeyboardKey.backspace): nada,
// const SingleActivator(LogicalKeyboardKey.backspace): nada,
const SingleActivator(LogicalKeyboardKey.enter): nada,
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
const SingleActivator(LogicalKeyboardKey.comma): nada,
@@ -610,7 +610,7 @@ class _BoardCardState extends State<_BoardCard> {
const SingleActivator(LogicalKeyboardKey.keyE): nada,
const SingleActivator(LogicalKeyboardKey.keyN): nada,
const SingleActivator(LogicalKeyboardKey.delete): nada,
const SingleActivator(LogicalKeyboardKey.backspace): nada,
// const SingleActivator(LogicalKeyboardKey.backspace): nada,
const SingleActivator(LogicalKeyboardKey.enter): nada,
const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,
const SingleActivator(LogicalKeyboardKey.comma): nada,
@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/shared/callback_shortcuts.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -147,7 +150,14 @@ class BoardShortcutContainer extends StatelessWidget {
if (focusScope.value.length != 1) {
return false;
}
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
NavigatorOkCancelDialog(
message: LocaleKeys.grid_row_deleteCardPrompt.tr(),
onOkPressed: () {
context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));
},
).show(context);
return true;
}
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
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';
@@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
@@ -90,12 +92,11 @@ class _CalendarPageState extends State<CalendarPage> {
@override
void initState() {
super.initState();
_calendarState = GlobalKey<MonthViewState>();
_calendarBloc = CalendarBloc(
databaseController: widget.databaseController,
)..add(const CalendarEvent.initial());
super.initState();
}
@override
@@ -378,13 +379,7 @@ class UnscheduledEventsButton extends StatefulWidget {
}
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
@@ -5,12 +7,10 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart
import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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';
/// Widget that displays a list of settings that alters the appearance of the
@@ -30,6 +30,12 @@ class CalendarLayoutSetting extends StatefulWidget {
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
final PopoverMutex popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
@@ -325,7 +331,6 @@ Widget _toggleItem({
Toggle(
value: value,
onChanged: (value) => onToggle(!value),
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
@@ -1,6 +1,3 @@
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
@@ -9,6 +6,7 @@ import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart';
@@ -16,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
@@ -24,7 +23,6 @@ import '../../application/row/row_controller.dart';
import '../../tab_bar/tab_bar_view.dart';
import '../../widgets/row/row_detail.dart';
import '../application/grid_bloc.dart';
import 'grid_scroll.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
@@ -504,7 +502,10 @@ class _PositionedCalculationsRowState
left: 0,
right: 0,
child: Container(
margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding + 40),
margin: EdgeInsets.only(
left:
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
),
padding: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
@@ -16,6 +14,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalculateCell extends StatefulWidget {
@@ -141,11 +140,14 @@ class _CalculateCellState extends State<CalculateCell> {
TextSpan(
text: widget.calculation!.calculationType.shortLabel
.toUpperCase(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: ' '),
TextSpan(
text: calculateValue,
style: const TextStyle(fontWeight: FontWeight.w500),
style: context
.tooltipTextStyle()
?.copyWith(fontWeight: FontWeight.w500),
),
],
),
@@ -1,18 +1,19 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class CheckboxFilterChoicechip extends StatefulWidget {
@@ -30,9 +31,9 @@ class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
@override
void initState() {
super.initState();
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const CheckboxFilterEditorEvent.initial());
super.initState();
}
@override
@@ -82,6 +83,12 @@ class CheckboxFilterEditor extends StatefulWidget {
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/checklist_filter_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../condition_button.dart';
import '../../disclosure_button.dart';
import '../../filter_info.dart';
@@ -23,20 +25,20 @@ class ChecklistFilterChoicechip extends StatefulWidget {
}
class _ChecklistFilterChoicechipState extends State<ChecklistFilterChoicechip> {
late ChecklistFilterEditorBloc bloc;
late PopoverMutex popoverMutex;
late final ChecklistFilterEditorBloc bloc;
final PopoverMutex popoverMutex = PopoverMutex();
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo);
bloc.add(const ChecklistFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
popoverMutex.dispose();
super.dispose();
}
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/number_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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 '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class NumberFilterChoiceChip extends StatefulWidget {
@@ -64,6 +66,12 @@ class NumberFilterEditor extends StatefulWidget {
class _NumberFilterEditorState extends State<NumberFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../disclosure_button.dart';
import '../../filter_info.dart';
import '../choicechip.dart';
import 'condition_list.dart';
import 'option_list.dart';
import 'select_option_loader.dart';
@@ -29,6 +31,7 @@ class _SelectOptionFilterChoicechipState
@override
void initState() {
super.initState();
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo,
@@ -43,7 +46,6 @@ class _SelectOptionFilterChoicechipState
);
}
bloc.add(const SelectOptionFilterEditorEvent.initial());
super.initState();
}
@override
@@ -90,6 +92,12 @@ class SelectOptionFilterEditor extends StatefulWidget {
class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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 '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TextFilterChoicechip extends StatelessWidget {
@@ -72,6 +74,12 @@ class TextFilterEditor extends StatefulWidget {
class _TextFilterEditorState extends State<TextFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/time_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.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 '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TimeFilterChoiceChip extends StatefulWidget {
@@ -64,6 +66,12 @@ class TimeFilterEditor extends StatefulWidget {
class _TimeFilterEditorState extends State<TimeFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
@@ -10,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/field/field_controller.dart';
@@ -35,15 +36,15 @@ class GridCreateFilterList extends StatefulWidget {
}
class _GridCreateFilterListState extends State<GridCreateFilterList> {
late GridCreateFilterBloc editBloc;
late final GridCreateFilterBloc editBloc;
@override
void initState() {
super.initState();
editBloc = GridCreateFilterBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
)..add(const GridCreateFilterEvent.initial());
super.initState();
}
@override
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@@ -5,9 +7,7 @@ import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bl
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.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';
import 'create_filter_list.dart';
@@ -77,13 +77,7 @@ class AddFilterButton extends StatefulWidget {
}
class _AddFilterButtonState extends State<AddFilterButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
final PopoverController popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -1,6 +1,7 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'choicechip/checkbox.dart';
import 'choicechip/checklist/checklist.dart';
import 'choicechip/date.dart';
@@ -8,7 +9,6 @@ import 'choicechip/number.dart';
import 'choicechip/select_option/select_option.dart';
import 'choicechip/text.dart';
import 'choicechip/url.dart';
import 'choicechip/time.dart';
import 'filter_info.dart';
class FilterMenuItem extends StatelessWidget {
@@ -23,15 +23,14 @@ class FilterMenuItem extends StatelessWidget {
FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo),
FieldType.MultiSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.Number =>
NumberFilterChoiceChip(filterInfo: filterInfo),
FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo),
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
FieldType.Time =>
TimeFilterChoiceChip(filterInfo: filterInfo),
// FieldType.Time =>
// TimeFilterChoiceChip(filterInfo: filterInfo),
_ => const SizedBox(),
};
}
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@@ -9,7 +11,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
@@ -41,13 +42,12 @@ class GridFieldCell extends StatefulWidget {
}
class _GridFieldCellState extends State<GridFieldCell> {
final PopoverController popoverController = PopoverController();
late final FieldCellBloc _bloc;
late PopoverController popoverController;
@override
void initState() {
super.initState();
popoverController = PopoverController();
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
@@ -139,7 +140,9 @@ class _GridHeaderState extends State<_GridHeader> {
}
Widget _cellLeading() {
return SizedBox(width: GridSize.horizontalHeaderPadding + 40);
return SizedBox(
width: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
);
}
}
Loaded 100 of 1106 files, more files were not shown because too many files have changed in this diff. Show more