Compare commits

..
41 Commits
Author SHA1 Message Date
Richard Shiue e788c71602 fix: padding issue with the select tags (#3929)
* fix: padding issue with the select tags

* chore: better solution from Lucas
2023-11-13 21:41:25 +08:00
Emil 7de2131431 chore: update Russian translations (#3823) 2023-11-13 19:36:41 +08:00
Richard Shiue 47f3702ca9 fix: integration test on kanban (#3928) 2023-11-13 19:32:11 +08:00
Lucas.Xu 3708a5b86a feat: add alignment and indent/outdent toolbar item (#3927)
* chore: rename mobile list toolbar item

* feat: add alignment and indent/outdent toolbar item

* feat: adjust link menu on mobile platform
2023-11-13 18:08:39 +08:00
Richard ShiueandMathias Mogensen a63a7ea611 feat: hidden kanban groups (#3907)
* feat: hide/unhide ui

* chore: implement collapsible side bar and adjust group header (#2)

* refactor: hidden columns into own file

* chore: adjust new group button position

* fix: flowy icon buton secondary color bleed

* chore: some UI adjustments

* fix: some regressions

* chore: proper group is_visible fetching

* chore: use a bloc to manage hidden groups

* fix: hiding groups not working

* chore: implement hidden group popups

* chore: proper ungrouped item column management

* chore: remove ungrouped items button

* chore: flowy hover build

* fix: clean up code

* test: integration tests

* fix: not null promise on null value

* fix: hide and unhide multiple groups

* chore: i18n and code review

* chore: missed review

* fix: rust-lib-test

* fix: dont completely remove flowyiconhovercolor

* chore: apply suggest

* fix: number of rows inside hidden groups not updating properly

* fix: hidden groups disappearing after collapse

* fix: hidden group title alignment

* fix: insert newly unhidden groups into the correct position

* chore: adjust padding all around

* feat: reorder hidden groups

* chore: adjust padding

* chore: collapse hidden groups section persist

* chore: no status group at beginning

* fix: hiding groups when grouping with other types

* chore: disable rename groups that arent supported

* chore: update appflowy board ref

* chore: better naming

* test: fix tests

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2023-11-13 16:14:31 +08:00
Kilu.He 7867f0366e feat: support the operations of field in the grid of tauri (#3906)
* feat: support the operations of field in the grid of tauri

* fix: performance optimizate
2023-11-13 14:16:32 +08:00
Lucas.Xu 251c6d22b2 fix: 0.3.8 known issues (#3912)
* fix: add a left padding to align the document and grid field

* fix: emoji picker in the slash menu is too small

* fix: replace the delete icon color with black

* fix: improve snackbar background color

* fix: cannot add new line after toggle list

* feat: set ⭐ as the default icon of getting started

* fix: the titlebar overflows when the title level is too deep

* fix: integration test

* fix: openAI hint text overflow

* fix: integration tests
2023-11-13 12:00:03 +08:00
Lucas.Xu 7cee8e392f feat: adjust cover plugin and support recent section on mobile platform (#3921) 2023-11-13 10:07:46 +08:00
Richard Shiue 765103dd22 chore: share button text color (#3913) 2023-11-12 23:41:10 +08:00
Nathan.fooo 50e612511d feat: using workspace crate deps (#3924)
* chore: workspace deps

* chore: use workspace deps
2023-11-12 21:18:27 +08:00
Nathan.fooo 7eb20b232a feat: adding suffix for user data folder when current cloud type is appflowy cloud (#3918)
* fix: load database fail caused by spawning long run task

* chore: yield long run task

* chore: fmt

* chore: update client api

* feat: copy data between server

* ci: fix af cloud test
2023-11-12 18:00:07 +08:00
Kritarth Sharma 3c7e636b65 fix: added missing hover tooltip to toolbar item (#3786) 2023-11-10 15:42:40 +08:00
Mathias Mogensen 889a313cc2 chore: update to version 0.3.8 (#3902)
* chore: update to version 0.3.8

* chore: Update CHANGELOG.md

* chore: Update CHANGELOG.md
2023-11-09 13:11:33 +08:00
Lucas.Xu 9586ea0e6f feat: display the titles of a view's ancestors and the view's title on the title bar. (#3898)
* feat: add no pages inside tips

* feat: show view's ancestors (include itself) title on bar

* feat: show view's ancestors (include itself) title on bar

* test: add integration tests

* fix: integration tests
2023-11-09 13:11:13 +08:00
Mathias Mogensen 42e7317cd4 fix: notifications setting (#3903)
* fix: notifications setting

* fix: remove dependency in reminder bloc

* test: remove redundant lines
2023-11-09 00:32:10 +01:00
Mathias Mogensen 17651bf64c feat: show notes icon when notes is not empty (#3893)
* feat: show notes icon when notes is not empty

* fix: redundant clone

* chore: update collab and fix after merging main
2023-11-09 00:30:50 +01:00
Nathan.fooo 73f1c211c2 fix: Invalid refresh token (#3879)
* chore: update client api

* chore: update client api

* chore: update client api

* chore: fix clippy

* chore: fix clippy

* ci: fix

* chore: update client api
2023-11-08 21:48:17 +08:00
Lucas.Xu afc6473582 feat: adjust toggle list, callout, quote and divider on mobile (#3894)
* feat: adjust toggle list block

* feat: show block actions when tapping divider

* feat: add toggle list and callout to toolbar

* feat: refactor the emoji picker button

* fix: toggle list integration tests
2023-11-08 21:10:29 +08:00
Kilu.He 663f9d3423 fix: support inserting grid block in editor (#3875)
* fix: support inserting grid block in editor

* feat: support adding view in table

* feat: support the operations of row in tauri grid
2023-11-08 14:13:17 +08:00
Generic-Ripe 5d4142d5b6 chore: update sv.json (#3896)
* Update sv.json

Some small typos

* Rename sv.json to sv-SE.json
2023-11-08 10:14:13 +08:00
Mathias Mogensen fe23183aef feat: add placeholder to card editor (#3870) 2023-11-07 15:58:22 +01:00
Lucas.Xu 8116ea1dba feat: adjust math_equation block and image block on mobile platform (#3890)
* feat: add image toolbar entry

* feat: add ... buttos on math_equation and image block

* fix: review issues

* feat: add copy link and save image to gallery

* feat: support redo / undo on mobile toolbar
2023-11-07 15:24:32 +08:00
Lucas.Xu 3e6529aeb8 chore: add Roboto Mono font family and its regular and italic variants as assets (#3876) 2023-11-06 16:44:21 +08:00
Richard Shiueandnathan c4fc60612f feat: add new group (#3854)
* feat: implement backend logic

* fix: did_create_row not working properly

* fix: did_delete_group not working properly

* fix: test

* chore: fix clippy

* fix: new card not editable and in wrong position

* feat: imlement UI for add new stack

* test: add integration test

* chore: i18n

* chore: remove debug message

* chore: merge conflict

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-11-06 16:17:05 +08:00
Richard Shiue 4d82bb5322 chore: move field width to field settings (#3830)
* refactor: remove unnecessary builder

* feat: add width to field settings

* refactor: field settings logic

* chore: oopsies

* chore: implement UI

* chore: remove GridFieldCellEquatable

* test: rust-lib test fix
2023-11-06 14:48:59 +08:00
Nathan.foooandRichard Shiue 2b684ae7bf test: rename group test (#3790)
* refactor: rename group

* test: add rename group test

* chore: add group operation interceptor

* refactor: impl interceptor trait

* chore: update type option when group change

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2023-11-06 14:39:31 +08:00
Richard Shiue eb54cf99d4 fix: calendar event editor field name overflow (#3877) 2023-11-06 13:03:32 +08:00
Affif MukhlashinandLucas.Xu 85af82acbe chore: update translations for Indonesian (#3864)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-06 11:05:42 +08:00
Nathan.fooo 3434d447a7 chore: update collab rev (#3874) 2023-11-06 00:47:20 +08:00
Nathan.fooo 1025b6d553 fix: af cloud sync auth (#3873)
* feat: open workspace

* chore: update env docs

* fix: invalid user callback

* fix: token invalid

* chore: update

* chore: update

* chore: update

* chore: fix test

* chore: fix tauri build
2023-11-05 14:00:24 +08:00
Mathias Mogensen b35d6131d4 feat: inline page reference (#3859)
* feat: more methods of inserting page reference

* test: add tests for inserting document reference

* chore: remove unused import

* chore: update editor ref

* tests: fix test with an interim solution
2023-11-03 21:30:24 +01:00
Yijing Huang bc502c9c5b chore: improve share button text color (#3868) 2023-11-03 12:09:12 -07:00
Kilu.Heandfangwufeng-v 5f49c1748f Fix/tauri warning to error (#3869)
* feat: sort basic function

* fix: eslint error

* fix: deal with conflict

* fix: prevent submit eslint warning code

* fix: modify tauri warning to error

---------

Co-authored-by: fangwufeng-v <fangwufeng.v@gmail.com>
2023-11-03 15:13:49 +08:00
Yijing HuangandLucas.Xu 73ea1a0685 feat:add toast in trash and confirm dialog + fix issues from launch review (#3787)
* chore: improve trash button

* feat: improve restore all&delete all

* refactor: add showFlowyMobileConfirmDialog

* feat: add toast in delete/restore single file

* refactor: refactor to TrashActionType enum

* fix: text invisible in signin page in dark mode

* feat: add FlowyMobileErrorStateContainer to display error state

* refactor: add FlowyMobileStateContainer to handle empty or error state

- Replace MobileErrorPage by FlowyMobileStateContainer.error
- Implement app version in reporting issue on github
- Implement FlowyMobileStateContainer in trash,setting,favorite and mobile view page

* refactor: unify bottom sheet style

- Unify bottom sheet style in add new page, page action, and trash action
- Add icon color in BottomSheetActionWidget for future use
- Add theme color in MobileBottomSheetDragHandle

* chore: unify Appbar style

* chore: remove the more button when trash list is empty

* fix: show bottom sheet error

* fix: fix merge and ui issue

* refactor: refactor ViewPageBottomSheet and origanize code

* chore: add icon color for favorite button

* fix: add missing icon color and delete comments

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-02 11:15:15 -07:00
Mathias Mogensen a93d325e6a fix: notification actions lemonade darkmode (#3862) 2023-11-02 17:17:30 +01:00
Kilu.He dc0af0f4c1 feat: support convert external data to nested json (#3848)
* feat: support convert external data to nested json

* fix: add some comment

* fix: code review

* fix: code review

* fix: code view

* fix: code view

* fix: update tauri cargo lock file

* fix: remove reduant function

* fix: parse dir attribute in element

* fix: add comment about parse dir

* fix: code review

* fix: code review

* fix: code review

* fix: code review
2023-11-02 22:13:29 +08:00
Lucas.Xu 1ad85416d8 fix: unable to build macOS universal package on x86 machine (#3863)
* chore: don't use cache when building release package

* fix: set ARCHS = ARCHS_STANDARD on macOS
2023-11-02 15:24:35 +08:00
Lucas.Xu c34a7a92fb feat: support customizing page icon (#3849)
* chore: don't use cache when building release package

* feat: refactor icon widget design

* feat: sync the emoji between page and view

* feat: use cache to store the emoji data to prevent reloading

* feat: customize the emoji item builder

* feat: add i18n and shuffle emoji button

* fix: integration test

* feat: replace emoji picker in Grid and slash menu

* feat: support adding icon on mobile platform

* feat: support adding and removing icon on mobile

* test: add integration tests
2023-11-02 15:24:17 +08:00
Nathan.fooo 21d34d1fe0 chore: update collab rev (#3852) 2023-11-01 14:47:25 +08:00
Nathan.fooo 3e088d48ac refactor: fav and workspace (#3837)
* refactor: workspace

* chore: update collab rev

* test: add data migration test

* fix: test

* fix: tauri build

* test: fix bloc test

* test: fix bloc test

* test: fix bloc test

* chore: restore magic codde
2023-11-01 11:45:35 +08:00
Mathias Mogensen 54dbcb7c5e fix: link to page dialog offset (#3840) 2023-10-31 20:11:32 +01:00
752 changed files with 18132 additions and 7951 deletions

No files matched your search

-24
View File
@@ -59,7 +59,6 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -70,11 +69,6 @@ jobs:
components: rustfmt
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites
working-directory: frontend
run: |
@@ -151,7 +145,6 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -162,11 +155,6 @@ jobs:
components: rustfmt
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites
working-directory: frontend
run: |
@@ -257,7 +245,6 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -266,11 +253,6 @@ jobs:
targets: ${{ matrix.job.targets }}
components: rustfmt
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites
working-directory: frontend
run: |
@@ -366,7 +348,6 @@ jobs:
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -377,11 +358,6 @@ jobs:
components: rustfmt
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: appflowy-lib-cache
key: ${{ matrix.job.os }}-${{ matrix.job.target }}
- name: Install prerequisites
working-directory: frontend
run: |
+19
View File
@@ -1,5 +1,24 @@
# Release Notes
## Version 0.3.8 - 11/13/2023
### New Features
- Support hiding any stack in a board
- Support customizing page icons in menu
- Display visual hint when card contains notes
- Quick action for adding new stack to a board
- Support more ways of inserting page references in documents
- Shift + click on a checkbox to power toggle its children
### Bug fixes
- Improved color of the "Share"-button text
- Text overflow issue in Calendar properties
- Default font (Roboto) added to application
- Placeholder added for the editor inside a Card
- Toggle notifications in settings have been fixed
- Dialog for linking board/grid/calendar opens in correct position
- Quick add Card in Board at top, correctly adds a new Card at the top
## Version 0.3.7 - 10/30/2023
### New Features
-8
View File
@@ -314,13 +314,5 @@
"cwd": "${workspaceFolder}/appflowy_flutter"
}
},
{
"label": "AF: Generate AppFlowyEnv",
"type": "shell",
"command": "dart run build_runner build --delete-conflicting-outputs",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend"
}
}
]
}
+1 -1
View File
@@ -25,7 +25,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.3.7"
CURRENT_APP_VERSION = "0.3.8"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+31 -13
View File
@@ -1,23 +1,41 @@
# Initial Setup
# 1. Copy the dev.env file to .env:
# cp dev.env .env
# 2. Alternatively, you can generate the .env file using the "Generate Env File" task in VSCode.
# Configuring Cloud Type
# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are:
# 1. Copy the dev.env file to .env:
# cp dev.env .env
# Update the environment parameters as needed.
# 2. Generate the env.dart from this .env file:
# You can use the "Generate Env File" task in VSCode.
# Alternatively, execute the following commands:
# cd appflowy_flutter
# dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
# Cloud Type Configuration
# Use this configuration file to specify the cloud type and its associated settings. The available cloud types are:
# Local: 0
# Supabase: 1
# AppFlowy Cloud: 2
# By default, it's set to Local.
CLOUD_TYPE=0
# Supabase Configuration
# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
SUPABASE_URL=replace-with-your-supabase-url
SUPABASE_ANON_KEY=replace-with-your-supabase-key
# If using Supabase (CLOUD_TYPE=1), provide the following details:
SUPABASE_URL=
SUPABASE_ANON_KEY=
# AppFlowy Cloud Configuration
# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations:
APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url
APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url
APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url
# If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details:
# For instance:
# APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx
# APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx
# APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx
#
# Local host machine(For local develop)
# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000
# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws
# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998
APPFLOWY_CLOUD_BASE_URL=
APPFLOWY_CLOUD_WS_BASE_URL=
APPFLOWY_CLOUD_GOTRUE_URL=
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -32,10 +33,12 @@ void main() {
await tester.tap(
find
.descendant(
of: find.byType(AppFlowyGroupHeader),
matching: find.byType(FlowySvg),
of: find.byType(BoardColumnHeader),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
),
)
.first,
.at(1),
);
await tester.pumpAndSettle();
@@ -77,7 +80,7 @@ void main() {
of: find.byType(AppFlowyGroupFooter),
matching: find.byType(FlowySvg),
)
.first,
.at(1),
);
await tester.pumpAndSettle();
@@ -0,0 +1,98 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board hide groups test', () {
testWidgets('expand/collapse hidden groups', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
final collapseFinder = find.byFlowySvg(FlowySvgs.pull_left_outlined_s);
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
// Is expanded by default
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
// Collapse hidden groups
await tester.tap(collapseFinder);
await tester.pumpAndSettle();
// Is collapsed
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
// Expand hidden groups
await tester.tap(expandFinder);
await tester.pumpAndSettle();
// Is expanded
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
});
testWidgets('hide first group, and show it again', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
// Tap the options of the first group
final optionsFinder = find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first;
await tester.tap(optionsFinder);
await tester.pumpAndSettle();
// Tap the hide option
await tester.tap(find.byFlowySvg(FlowySvgs.hide_s));
await tester.pumpAndSettle();
int shownGroups =
tester.widgetList(find.byType(BoardColumnHeader)).length;
// We still show Doing, Done, No Status
expect(shownGroups, 3);
final hiddenCardFinder = find.byType(HiddenGroupCard);
await tester.hoverOnWidget(hiddenCardFinder);
await tester.tap(find.byFlowySvg(FlowySvgs.show_m));
await tester.pumpAndSettle();
shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;
expect(shownGroups, 4);
});
});
}
extension FlowySvgFinder on CommonFinders {
Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);
}
class _FlowySvgFinder extends MatchFinder {
_FlowySvgFinder(this.svg);
final FlowySvgData svg;
@override
String get description => 'flowy_svg "$svg"';
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
return widget is FlowySvg && widget.svg == svg;
}
}
@@ -1,11 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -46,5 +49,56 @@ void main() {
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
});
testWidgets('add new group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
// assert number of groups
tester.assertNumberOfGroups(4);
// scroll the board horizontally to ensure add new group button appears
await tester.scrollBoardToEnd();
// assert and click on add new group button
tester.assertNewGroupTextField(false);
await tester.tapNewGroupButton();
tester.assertNewGroupTextField(true);
// enter new group name and submit
await tester.enterNewGroupName('needs design', submit: true);
// assert number of groups has increased
tester.assertNumberOfGroups(5);
// assert text field has disappeared
await tester.scrollBoardToEnd();
tester.assertNewGroupTextField(false);
// click on add new group button
await tester.tapNewGroupButton();
tester.assertNewGroupTextField(true);
// type some things
await tester.enterNewGroupName('needs planning', submit: false);
// click on clear button and assert empty contents
await tester.clearNewGroupTextField();
// press escape to cancel
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
tester.assertNewGroupTextField(false);
// click on add new group button
await tester.tapNewGroupButton();
tester.assertNewGroupTextField(true);
// press elsewhere to cancel
await tester.tap(find.byType(AppFlowyBoard));
await tester.pumpAndSettle();
tester.assertNewGroupTextField(false);
});
});
}
@@ -42,7 +42,6 @@ void main() {
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// After select the emoji, the EmojiButton will show up
@@ -60,12 +59,10 @@ void main() {
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Update existing selected emoji
await tester.tapButton(find.byType(EmojiButton));
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
// The emoji already displayed in the row banner
@@ -89,7 +86,6 @@ void main() {
await tester.openFirstRowDetailPage();
await tester.hoverRowBanner();
await tester.openEmojiPicker();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
// Remove the emoji
@@ -0,0 +1,136 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/keyboard.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('insert inline document reference', () {
testWidgets('insert by slash menu', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await triggerReferenceDocumentBySlashMenu(tester);
// Search for prefix of document
await enterDocumentText(tester);
// Select result
final optionFinder = find.descendant(
of: find.byType(LinkToPageMenu),
matching: find.text(name),
);
await tester.tap(optionFinder);
await tester.pumpAndSettle();
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
});
testWidgets('insert by `[[` character shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.ime.insertText('[[');
await tester.pumpAndSettle();
// Select result
await tester.editor.tapAtMenuItemWithName(name);
await tester.pumpAndSettle();
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
});
testWidgets('insert by `+` character shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final name = await createDocumentToReference(tester);
await tester.editor.tapLineOfEditorAt(0);
await tester.pumpAndSettle();
await tester.ime.insertText('+');
await tester.pumpAndSettle();
// Select result
await tester.editor.tapAtMenuItemWithName(name);
await tester.pumpAndSettle();
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
});
});
}
Future<String> createDocumentToReference(WidgetTester tester) async {
final name = 'document_${uuid()}';
await tester.createNewPageWithName(
name: name,
layout: ViewLayoutPB.Document,
openAfterCreated: false,
);
// This is a workaround since the openAfterCreated
// option does not work in createNewPageWithName method
await tester.tap(find.byType(SingleInnerViewItem).first);
await tester.pumpAndSettle();
return name;
}
Future<void> triggerReferenceDocumentBySlashMenu(WidgetTester tester) async {
await tester.editor.showSlashMenu();
await tester.pumpAndSettle();
// Search for referenced document action
await enterDocumentText(tester);
// Select item
await FlowyTestKeyboard.simulateKeyDownEvent(
[
LogicalKeyboardKey.enter,
],
tester: tester,
);
await tester.pumpAndSettle();
}
Future<void> enterDocumentText(WidgetTester tester) async {
await FlowyTestKeyboard.simulateKeyDownEvent(
[
LogicalKeyboardKey.keyD,
LogicalKeyboardKey.keyO,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyU,
LogicalKeyboardKey.keyM,
LogicalKeyboardKey.keyE,
LogicalKeyboardKey.keyN,
LogicalKeyboardKey.keyT,
],
tester: tester,
);
await tester.pumpAndSettle();
}
@@ -16,6 +16,8 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_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;
import 'document_inline_page_reference_test.dart'
as document_inline_page_reference_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -35,4 +37,5 @@ void startTesting() {
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
document_inline_page_reference_test.main();
}
@@ -1,4 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -17,7 +22,6 @@ void main() {
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document cover
await tester.editor.tapOnAddCover();
@@ -53,15 +57,10 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
tester.expectToSeeDocumentIcon('⭐️');
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.editor.tapGettingStartedIcon();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
@@ -73,13 +72,11 @@ void main() {
// Add the icon back for further testing
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.tapEmoji('😀');
tester.expectToSeeDocumentIcon('😀');
// Change the document icon
await tester.editor.tapOnIconWidget();
await tester.switchToEmojiList();
await tester.tapEmoji('😅');
tester.expectToSeeDocumentIcon('😅');
@@ -93,19 +90,15 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeDocumentIcon(null);
tester.expectToSeeDocumentIcon('⭐️');
tester.expectToSeeNoDocumentCover();
// Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeePluginAddCoverAndIconButton();
// Insert a document icon
await tester.editor.tapAddIconButton();
await tester.switchToEmojiList();
await tester.editor.tapGettingStartedIcon();
await tester.tapEmoji('😀');
// Insert a document cover
await tester.editor.hoverOnCoverToolbar();
await tester.editor.tapOnAddCover();
// Expect to see the icon and cover at the same time
@@ -116,5 +109,48 @@ void main() {
await tester.editor.hoverOnCoverToolbar();
tester.expectToSeeEmptyDocumentHeaderToolbar();
});
testWidgets('shuffle icon', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.tapGettingStartedIcon();
// click the shuffle button
await tester.tapButton(
find.byTooltip(LocaleKeys.emoji_random.tr()),
);
tester.expectDocumentIconNotNull();
});
testWidgets('change skin tone', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.editor.tapGettingStartedIcon();
final searchEmojiTextField = find.byWidgetPredicate(
(widget) =>
widget is TextField &&
widget.decoration!.hintText == LocaleKeys.emoji_search.tr(),
);
await tester.enterText(
searchEmojiTextField,
'hand',
);
// change skin tone
await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark);
// select an icon with skin tone
const hand = '👋🏿';
await tester.tapEmoji(hand);
tester.expectToSeeDocumentIcon(hand);
tester.expectViewHasIcon(
gettingStarted,
ViewLayoutPB.Document,
hand,
);
});
});
}
@@ -15,7 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Grid);
await insertInlinePage(tester, ViewLayoutPB.Grid);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
@@ -26,7 +26,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Board);
await insertInlinePage(tester, ViewLayoutPB.Board);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
@@ -37,7 +37,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Calendar);
await insertInlinePage(tester, ViewLayoutPB.Calendar);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
@@ -48,7 +48,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await insertingInlinePage(tester, ViewLayoutPB.Document);
await insertInlinePage(tester, ViewLayoutPB.Document);
final mentionBlock = find.byType(MentionPageBlock);
expect(mentionBlock, findsOneWidget);
@@ -59,7 +59,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
final pageName = await insertInlinePage(tester, ViewLayoutPB.Document);
// rename
const newName = 'RenameToNewPageName';
@@ -78,7 +78,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
final pageName = await insertInlinePage(tester, ViewLayoutPB.Grid);
// rename
await tester.hoverOnPageName(
@@ -98,7 +98,7 @@ void main() {
}
/// Insert a referenced database of [layout] into the document
Future<String> insertingInlinePage(
Future<String> insertInlinePage(
WidgetTester tester,
ViewLayoutPB layout,
) async {
@@ -110,15 +110,19 @@ Future<String> insertingInlinePage(
layout: layout,
openAfterCreated: false,
);
// create a new document
await tester.createNewPageWithName(
name: 'insert_a_inline_page_${layout.name}',
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
await tester.editor.showAtMenu();
await tester.editor.tapAtMenuItemWithName(name);
return name;
}
@@ -15,14 +15,23 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('toggle list in document', () {
Finder findToggleListIcon({
required bool isExpanded,
}) {
final turns = isExpanded ? 0.25 : 0.0;
return find.byWidgetPredicate(
(widget) => widget is AnimatedRotation && widget.turns == turns,
);
}
void expectToggleListOpened() {
expect(find.byIcon(Icons.arrow_drop_down), findsOneWidget);
expect(find.byIcon(Icons.arrow_right), findsNothing);
expect(findToggleListIcon(isExpanded: true), findsOneWidget);
expect(findToggleListIcon(isExpanded: false), findsNothing);
}
void expectToggleListClosed() {
expect(find.byIcon(Icons.arrow_drop_down), findsNothing);
expect(find.byIcon(Icons.arrow_right), findsOneWidget);
expect(findToggleListIcon(isExpanded: false), findsOneWidget);
expect(findToggleListIcon(isExpanded: true), findsNothing);
}
testWidgets('convert > to toggle list, and click the icon to close it',
@@ -63,7 +72,7 @@ void main() {
expect(find.text(text2, findRichText: true), findsOneWidget);
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
final toggleListIcon = find.byIcon(Icons.arrow_right);
await tester.tapButton(toggleListIcon);
// expect the toggle list to be closed
@@ -88,7 +97,7 @@ void main() {
await tester.ime.insertText('> $text');
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
final toggleListIcon = find.byIcon(Icons.arrow_right);
await tester.tapButton(toggleListIcon);
// Press the enter key
@@ -164,7 +173,7 @@ void main() {
// Press the enter key
// Click the toggle list icon to close it
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
final toggleListIcon = find.byIcon(Icons.arrow_right);
await tester.tapButton(toggleListIcon);
await tester.editor.updateSelection(
@@ -113,9 +113,9 @@ const _sample = r'''
[] Type followed by bullet or num to create a list.
[x] Click `+ New Page` button at the bottom of your sidebar to add a new page.
[x] Click `New Page` button at the bottom of your sidebar to add a new page.
[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
[] Click the plus sign next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.
---
* bulleted list 1
@@ -19,8 +19,7 @@ import 'document/document_test_runner.dart' as document_test_runner;
import 'empty_test.dart' as first_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'import_files_test.dart' as import_files_test;
import 'settings/user_icon_test.dart' as user_icon_test;
import 'settings/user_language_test.dart' as user_language_test;
import 'settings/settings_runner.dart' as settings_test_runner;
import 'share_markdown_test.dart' as share_markdown_test;
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'switch_folder_test.dart' as switch_folder_test;
@@ -75,8 +74,7 @@ void main() {
appearance_test_runner.main();
// User settings
user_icon_test.main();
user_language_test.main();
settings_test_runner.main();
if (isCloudEnabled) {
auth_test_runner.main();
@@ -0,0 +1,41 @@
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board add row test', () {
testWidgets('Add card from header', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.notifications);
await tester.pumpAndSettle();
final switchFinder = find.byType(Switch);
// Defaults to enabled
Switch switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, true);
// Disable
await tester.tap(switchFinder);
await tester.pumpAndSettle();
switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, false);
// Enable again
await tester.tap(switchFinder);
await tester.pumpAndSettle();
switchWidget = tester.widget(switchFinder);
expect(switchWidget.value, true);
});
});
}
@@ -0,0 +1,13 @@
import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'user_icon_test.dart' as user_icon_test;
import 'user_language_test.dart' as user_language_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
notifications_settings_test.main();
user_icon_test.main();
user_language_test.main();
}
@@ -99,7 +99,7 @@ void main() {
);
expect(
tester.findFavoritePageName(name),
findsNothing,
findsOneWidget,
);
},
);
@@ -127,7 +127,7 @@ void main() {
expect(
find.byWidgetPredicate(
(widget) =>
widget is ViewItem &&
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
),
@@ -144,12 +144,7 @@ void main() {
);
expect(
find.byWidgetPredicate(
(widget) =>
widget is ViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
),
tester.findAllFavoritePages(),
findsNWidgets(3),
);
@@ -163,12 +158,7 @@ void main() {
);
expect(
find.byWidgetPredicate(
(widget) =>
widget is ViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
),
tester.findAllFavoritePages(),
findsNothing,
);
},
@@ -0,0 +1,76 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/base.dart';
import '../util/common_operations.dart';
import '../util/expectation.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const emoji = '😁';
group('Icon', () {
testWidgets('Update page icon in sidebar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
await tester.createNewPageWithName(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInSidebarByName(
name: value.name,
parentName: gettingStarted,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
}
});
testWidgets('Update page icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
await tester.createNewPageWithName(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: emoji,
);
tester.expectViewHasIcon(
value.name,
value,
emoji,
);
tester.expectViewTitleHasIcon(
value.name,
value,
emoji,
);
}
});
});
}
@@ -1,8 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'sidebar_test.dart' as sidebar_test;
import 'sidebar_expand_test.dart' as sidebar_expanded_test;
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_test.dart' as sidebar_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -11,4 +12,5 @@ void startTesting() {
sidebar_test.main();
sidebar_expanded_test.main();
sidebar_favorite_test.main();
sidebar_icon_test.main();
}
@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -81,9 +83,15 @@ extension AppFlowyTestBase on WidgetTester {
}
Future<void> waitUntilSignInPageShow() async {
final finder = find.byType(GoButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
if (isCloudEnabled) {
final finder = find.byType(SignInAnonymousButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
} else {
final finder = find.byType(GoButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
}
}
Future<void> pumpUntilFound(
@@ -4,15 +4,18 @@ 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/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -22,13 +25,21 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'emoji.dart';
import 'util.dart';
extension CommonOperations on WidgetTester {
/// Tap the GetStart button on the launch page.
Future<void> tapGoButton() async {
// local version
final goButton = find.byType(GoButton);
await tapButton(goButton);
if (goButton.evaluate().isNotEmpty) {
await tapButton(goButton);
} else {
// cloud version
final anonymousButton = find.byType(SignInAnonymousButton);
await tapButton(anonymousButton);
}
if (Platform.isWindows) {
await pumpAndSettle(const Duration(milliseconds: 200));
@@ -434,6 +445,47 @@ extension CommonOperations on WidgetTester {
);
await tapButton(button);
}
// update the page icon in the sidebar
Future<void> updatePageIconInSidebarByName({
required String name,
required String parentName,
required ViewLayoutPB layout,
required String icon,
}) async {
final iconButton = find.descendant(
of: findPageName(
name,
layout: layout,
parentName: parentName,
),
matching:
find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),
);
await tapButton(iconButton);
await tapEmoji(icon);
await pumpAndSettle();
}
// update the page icon in the sidebar
Future<void> updatePageIconInTitleBarByName({
required String name,
required ViewLayoutPB layout,
required String icon,
}) async {
await openPage(
name,
layout: layout,
);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(name),
);
await tapButton(title);
await tapButton(find.byType(EmojiPickerButton));
await tapEmoji(icon);
await pumpAndSettle();
}
}
extension ViewLayoutPBTest on ViewLayoutPB {
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
@@ -38,7 +39,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
@@ -53,13 +53,14 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -618,7 +619,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> openEmojiPicker() async {
await tapButton(find.byType(EmojiPickerButton));
await tapButton(find.byType(EmojiSelectionMenu));
}
Future<void> tapDateCellInRowDetailPage() async {
@@ -1392,6 +1392,84 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findCreateButton);
}
void assertNumberOfGroups(int number) {
final groups = find.byType(BoardColumnHeader, skipOffstage: false);
expect(groups, findsNWidgets(number));
}
Future<void> scrollBoardToEnd() async {
final scrollable = find
.descendant(
of: find.byType(AppFlowyBoard),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable && widget.axis == Axis.horizontal,
),
)
.first;
await scrollUntilVisible(
find.byType(BoardTrailing),
300,
scrollable: scrollable,
);
}
Future<void> tapNewGroupButton() async {
final button = find.descendant(
of: find.byType(BoardTrailing),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
),
);
expect(button, findsOneWidget);
await tapButton(button);
}
void assertNewGroupTextField(bool isVisible) {
final textField = find.descendant(
of: find.byType(BoardTrailing),
matching: find.byType(TextField),
);
if (isVisible) {
expect(textField, findsOneWidget);
} else {
expect(textField, findsNothing);
}
}
Future<void> enterNewGroupName(String name, {required bool submit}) async {
final textField = find.descendant(
of: find.byType(BoardTrailing),
matching: find.byType(TextField),
);
await enterText(textField, name);
await pumpAndSettle();
if (submit) {
await testTextInput.receiveAction(TextInputAction.done);
await pumpAndSettle();
}
}
Future<void> clearNewGroupTextField() async {
final textField = find.descendant(
of: find.byType(BoardTrailing),
matching: find.byType(TextField),
);
await tapButton(
find.descendant(
of: textField,
matching: find.byWidgetPredicate(
(widget) =>
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m,
),
),
);
final textFieldWidget = widget<TextField>(textField);
assert(
textFieldWidget.controller != null &&
textFieldWidget.controller!.text.isEmpty,
);
}
Future<void> tapTabBarLinkedViewByViewName(String name) async {
final viewButton = findTabBarLinkViewByViewName(name);
await tapButton(viewButton);
@@ -1,15 +1,18 @@
import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.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/header/emoji_popover.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:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -54,7 +57,27 @@ class EditorOperations {
await tester.tapButtonWithName(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(find.byType(EmojiPopover), findsOneWidget);
expect(find.byType(FlowyEmojiPicker), findsOneWidget);
}
Future<void> tapGettingStartedIcon() async {
await tester.tapButton(
find.descendant(
of: find.byType(DocumentHeaderNodeWidget),
matching: find.findTextInFlowyText('⭐️'),
),
);
}
/// Taps on the 'Skin tone' button
///
/// Must call [tapAddIconButton] first.
Future<void> changeEmojiSkinTone(EmojiSkinTone skinTone) async {
await tester.tapButton(
find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()),
);
final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon));
await tester.tapButton(skinToneButton);
}
/// Taps the 'Remove Icon' button in the cover toolbar and the icon popover
@@ -62,7 +85,10 @@ class EditorOperations {
Finder button =
find.text(LocaleKeys.document_plugins_cover_removeIcon.tr());
if (isInPicker) {
button = find.descendant(of: find.byType(EmojiPopover), matching: button);
button = find.descendant(
of: find.byType(FlowyIconPicker),
matching: button,
);
}
await tester.tapButton(button);
@@ -143,7 +169,7 @@ class EditorOperations {
await tester.ime.insertCharacter('/');
}
/// trigger the slash command (selection menu)
/// trigger the mention (@) command
Future<void> showAtMenu() async {
await tester.ime.insertCharacter('@');
}
@@ -1,17 +1,14 @@
import 'package:flutter/material.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension EmojiTestExtension on WidgetTester {
/// Must call [openEmojiPicker] first
Future<void> switchToEmojiList() async {
final icon = find.byIcon(Icons.tag_faces);
await tapButton(icon);
}
Future<void> tapEmoji(String emoji) async {
final emojiWidget = find.text(emoji);
final emojiWidget = find.descendant(
of: find.byType(EmojiPicker),
matching: find.text(emoji),
);
await tapButton(emojiWidget);
}
}
@@ -5,13 +5,14 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
// const String readme = 'Read me';
const String gettingStarted = '⭐️ Getting started';
const String gettingStarted = 'Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
@@ -108,6 +109,13 @@ extension Expectation on WidgetTester {
expect(iconWidget, findsOneWidget);
}
void expectDocumentIconNotNull() {
final iconWidget = find.byWidgetPredicate(
(widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty,
);
expect(iconWidget, findsOneWidget);
}
void expectToSeeDocumentCover(CoverType type) {
final findCover = find.byWidgetPredicate(
(widget) => widget is DocumentCover && widget.coverType == type,
@@ -157,7 +165,7 @@ extension Expectation on WidgetTester {
}) {
return find.byWidgetPredicate(
(widget) =>
widget is ViewItem &&
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite &&
widget.view.name == name &&
@@ -166,6 +174,15 @@ extension Expectation on WidgetTester {
);
}
Finder findAllFavoritePages() {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
);
}
Finder findPageName(
String name, {
ViewLayoutPB layout = ViewLayoutPB.Document,
@@ -193,4 +210,24 @@ extension Expectation on WidgetTester {
matching: findPageName(name, layout: layout),
);
}
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
final pageName = findPageName(
name,
layout: layout,
);
final icon = find.descendant(
of: pageName,
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
}
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
final icon = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(emoji),
);
expect(icon, findsOneWidget);
}
}
@@ -48,6 +48,13 @@ PODS:
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_gallery_saver (2.0.2):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
@@ -68,6 +75,9 @@ PODS:
- FlutterMacOS
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- super_native_extensions (0.0.1):
- Flutter
- SwiftyGif (5.4.3)
@@ -86,6 +96,8 @@ DEPENDENCIES:
- flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`)
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -93,6 +105,7 @@ DEPENDENCIES:
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
@@ -101,6 +114,7 @@ SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FMDB
- ReachabilitySwift
- SDWebImage
- SwiftyGif
@@ -123,6 +137,10 @@ EXTERNAL SOURCES:
:path: Flutter
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
@@ -137,6 +155,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
@@ -155,6 +175,9 @@ SPEC CHECKSUMS:
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
@@ -164,6 +187,7 @@ SPEC CHECKSUMS:
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
+67 -63
View File
@@ -1,66 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>NSCameraUsageDescription</key>
<string>AppFlowy requires access to the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy requires access to the photo library.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>FLTEnableImpeller</key>
<false />
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
</dict>
</plist>
@@ -1,10 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
// ignore_for_file: non_constant_identifier_names
// Run `dart run build_runner build` to generate the json serialization If the
// file `env_serde.g.dart` is existed, delete it first.
//
// the file `env_serde.g.dart` will be generated in the same directory.
part 'env_serde.g.dart';
import 'package:json_annotation/json_annotation.dart';
part 'backend_env.g.dart';
@JsonSerializable()
class AppFlowyEnv {
+11 -3
View File
@@ -101,7 +101,10 @@ CloudType currentCloudType() {
final value = Env.cloudType;
if (value == 1) {
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
Log.error("Supabase is not configured");
Log.error(
"Supabase is not configured correctly. The values are: "
"url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}",
);
return CloudType.unknown;
} else {
return CloudType.supabase;
@@ -109,8 +112,13 @@ CloudType currentCloudType() {
}
if (value == 2) {
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) {
Log.error("AppFlowy cloud is not configured");
if (Env.afCloudBaseUrl.isEmpty ||
Env.afCloudWSBaseUrl.isEmpty ||
Env.afCloudGoTrueUrl.isEmpty) {
Log.error(
"AppFlowy cloud is not configured correctly. The values are: "
"baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}",
);
return CloudType.unknown;
} else {
return CloudType.appflowyCloud;
@@ -2,12 +2,22 @@ import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileRouterRecord {
PropertyValueNotifier<String> lastPushedRouter =
PropertyValueNotifier<String>('');
}
extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName;
push(
Uri(
path: view.routeName,
@@ -34,6 +34,7 @@ ThemeData getMobileThemeData(
//Snack bar
surface: Colors.white,
onSurface: _onSurfaceColor, // text/body color
surfaceVariant: const Color.fromARGB(255, 216, 216, 216),
)
: ColorScheme(
brightness: brightness,
@@ -64,7 +65,6 @@ ThemeData getMobileThemeData(
appBarTheme: AppBarTheme(
foregroundColor: mobileColorTheme.onBackground,
backgroundColor: mobileColorTheme.background,
elevation: 80,
centerTitle: false,
titleTextStyle: TextStyle(
color: mobileColorTheme.onBackground,
@@ -116,7 +116,7 @@ ThemeData getMobileThemeData(
foregroundColor: MaterialStateProperty.all(
mobileColorTheme.onBackground,
),
backgroundColor: MaterialStateProperty.all(Colors.white),
backgroundColor: MaterialStateProperty.all(mobileColorTheme.background),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
@@ -129,7 +129,7 @@ ThemeData getMobileThemeData(
),
),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 16),
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
),
// splash color
overlayColor: MaterialStateProperty.all(
@@ -224,6 +224,7 @@ ThemeData getMobileThemeData(
),
),
colorScheme: mobileColorTheme,
indicatorColor: Colors.blue,
extensions: [
AFThemeExtension(
warning: theme.yellow,
@@ -1,8 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart';
import 'package:appflowy/mobile/presentation/error/error_page.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -56,8 +56,11 @@ class _MobileViewPageState extends State<MobileViewPage> {
child: CircularProgressIndicator(),
);
} else if (!state.hasData) {
body = MobileErrorPage(
message: LocaleKeys.error_loadingViewError.tr(),
body = FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: state.error.toString(),
);
} else {
body = state.data!.fold((view) {
@@ -65,8 +68,11 @@ class _MobileViewPageState extends State<MobileViewPage> {
actions.add(_buildAppBarMoreButton(view));
return view.plugin().widgetBuilder.buildWidget(shrinkWrap: false);
}, (error) {
return MobileErrorPage(
message: error.toString(),
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
});
}
@@ -106,12 +112,26 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
final icon = view?.icon.value;
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: FlowyText.semibold(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
FlowyText(
'$icon ',
fontSize: 22.0,
),
Expanded(
child: FlowyText.regular(
view?.name ?? widget.title ?? '',
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
],
),
leading: AppBarBackButton(
onTap: () => context.pop(),
@@ -163,12 +183,21 @@ class _MobileViewPageState extends State<MobileViewPage> {
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
context.dispatchNotification(
const EditorNotification(type: EditorNotificationType.redo),
);
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
context.pop();
context.dispatchNotification(EditorNotification.redo());
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
@@ -1,146 +1,9 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({
required BuildContext context,
required WidgetBuilder builder,
}) async {
showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: true,
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
),
),
builder: builder,
);
}
enum MobileBottomSheetType {
view,
rename,
}
class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
@override
State<MobileViewItemBottomSheet> createState() =>
_MobileViewItemBottomSheetState();
}
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
@override
initState() {
super.initState();
type = widget.defaultType;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handler
const MobileBottomSheetDragHandler(),
// header
_buildHeader(),
const VSpace(8.0),
const Divider(),
// body
_buildBody(),
const VSpace(12.0),
],
);
}
Widget _buildHeader() {
switch (type) {
case MobileBottomSheetType.view:
case MobileBottomSheetType.rename:
// header
return MobileViewItemBottomSheetHeader(
showBackButton: type != MobileBottomSheetType.view,
view: widget.view,
onBack: () {
setState(() {
type = MobileBottomSheetType.view;
});
},
);
}
}
Widget _buildBody() {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
setState(() {
type = MobileBottomSheetType.rename;
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
}
},
);
case MobileBottomSheetType.rename:
return MobileBottomSheetRenameWidget(
name: widget.view.name,
onRename: (name) {
if (name != widget.view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
}
export 'bottom_sheet_action_widget.dart';
export 'bottom_sheet_add_new_page.dart';
export 'bottom_sheet_drag_handler.dart';
export 'bottom_sheet_rename_widget.dart';
export 'bottom_sheet_view_item_body.dart';
export 'bottom_sheet_view_item_header.dart';
export 'bottom_sheet_view_page.dart';
export 'default_mobile_action_pane.dart';
export 'show_mobile_bottom_sheet.dart';
@@ -1,8 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/box_container.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class BottomSheetActionWidget extends StatelessWidget {
const BottomSheetActionWidget({
@@ -10,42 +7,31 @@ class BottomSheetActionWidget extends StatelessWidget {
required this.svg,
required this.text,
required this.onTap,
this.iconColor,
});
final FlowySvgData svg;
final String text;
final VoidCallback onTap;
final Color? iconColor;
@override
Widget build(BuildContext context) {
return FlowyBoxContainer(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onTap();
},
enableFeedback: true,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowySvg(
svg,
size: const Size.square(24.0),
blendMode: BlendMode.dst,
),
const HSpace(6.0),
FlowyText(text),
const Spacer(),
],
),
),
final iconColor =
this.iconColor ?? Theme.of(context).colorScheme.onBackground;
return OutlinedButton.icon(
icon: FlowySvg(
svg,
size: const Size.square(22.0),
color: iconColor,
),
label: Text(text),
style: Theme.of(context)
.outlinedButtonTheme
.style
?.copyWith(alignment: Alignment.centerLeft),
onPressed: onTap,
);
}
}
@@ -1,8 +1,6 @@
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_action_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -21,46 +19,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handler
const MobileBottomSheetDragHandler(),
// header
MobileViewItemBottomSheetHeader(
showBackButton: false,
view: view,
onBack: () {},
),
const VSpace(8.0),
const Divider(),
// body
_AddNewPageBody(
onAction: onAction,
),
const VSpace(24.0),
],
);
}
}
class _AddNewPageBody extends StatelessWidget {
const _AddNewPageBody({
required this.onAction,
});
final void Function(ViewLayoutPB layout) onAction;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// rename, duplicate
// new document, new grid
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -69,6 +30,7 @@ class _AddNewPageBody extends StatelessWidget {
onTap: () => onAction(ViewLayoutPB.Document),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.grid_s,
@@ -78,10 +40,10 @@ class _AddNewPageBody extends StatelessWidget {
),
],
),
const VSpace(8),
// share, delete
// new board, new calendar
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -90,6 +52,7 @@ class _AddNewPageBody extends StatelessWidget {
onTap: () => onAction(ViewLayoutPB.Board),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.date_s,
@@ -0,0 +1,78 @@
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_action_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
enum BlockActionBottomSheetType {
delete,
duplicate,
insertAbove,
insertBelow,
}
// Only works on mobile.
class BlockActionBottomSheet extends StatelessWidget {
const BlockActionBottomSheet({
super.key,
required this.onAction,
this.extendActionWidgets = const [],
});
final void Function(BlockActionBottomSheetType layout) onAction;
final List<Widget> extendActionWidgets;
@override
Widget build(BuildContext context) {
return Column(
children: [
// insert above, insert below
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.arrow_up_s,
text: LocaleKeys.button_insertAbove.tr(),
onTap: () => onAction(BlockActionBottomSheetType.insertAbove),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.arrow_down_s,
text: LocaleKeys.button_insertBelow.tr(),
onTap: () => onAction(BlockActionBottomSheetType.insertBelow),
),
),
],
),
const VSpace(8),
// duplicate, delete
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_duplicate_m,
text: LocaleKeys.button_duplicate.tr(),
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
text: LocaleKeys.button_delete.tr(),
onTap: () => onAction(BlockActionBottomSheetType.delete),
),
),
],
),
const VSpace(8),
...extendActionWidgets,
],
);
}
}
@@ -12,7 +12,7 @@ class MobileBottomSheetDragHandler extends StatelessWidget {
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2.0),
color: Theme.of(context).colorScheme.onSecondary,
color: Theme.of(context).hintColor,
),
),
);
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
enum MobileViewItemBottomSheetBodyAction {
@@ -26,12 +27,11 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// rename, duplicate
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -42,6 +42,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_duplicate_m,
@@ -53,10 +54,11 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
),
],
),
const VSpace(8),
// share, delete
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -67,6 +69,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
@@ -78,13 +81,15 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
),
],
),
const VSpace(8),
// remove from favorites
// remove from favorites/add to favorites
BottomSheetActionWidget(
svg: isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
//TODO(yijing): switch to theme color
iconColor: isFavorite ? Colors.yellow : null,
text: isFavorite
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
@@ -1,6 +1,6 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileViewItemBottomSheetHeader extends StatelessWidget {
const MobileViewItemBottomSheetHeader({
@@ -16,8 +16,8 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// back button,
showBackButton
@@ -31,14 +31,23 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
),
),
)
: const HSpace(40.0),
: const SizedBox.shrink(),
// title
FlowyText.regular(
view.name,
fontSize: 16.0,
Expanded(
child: Text(
view.name,
style: theme.textTheme.labelSmall,
),
),
// placeholder, ensure the title is centered
const HSpace(40.0),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
)
],
);
}
@@ -1,10 +1,6 @@
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_action_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -47,21 +43,18 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handler
const MobileBottomSheetDragHandler(),
// header
_buildHeader(),
const VSpace(8.0),
const Divider(),
// body
_buildBody(),
const VSpace(24.0),
],
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
),
);
}
@@ -125,37 +118,38 @@ class MobileViewBottomSheetBody extends StatelessWidget {
Widget build(BuildContext context) {
final isFavorite = view.isFavorite;
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// undo, redo
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_undo_m,
text: LocaleKeys.toolbar_undo.tr(),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.undo,
),
),
),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_redo_m,
text: LocaleKeys.toolbar_redo.tr(),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.redo,
),
),
),
],
),
// Row(
// mainAxisSize: MainAxisSize.max,
// children: [
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_undo_m,
// text: LocaleKeys.toolbar_undo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.undo,
// ),
// ),
// ),
// const HSpace(8),
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_redo_m,
// text: LocaleKeys.toolbar_redo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.redo,
// ),
// ),
// ),
// ],
// ),
// const VSpace(8),
// rename, duplicate
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -166,6 +160,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_duplicate_m,
@@ -177,10 +172,11 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
],
),
const VSpace(8),
// share, delete
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: BottomSheetActionWidget(
@@ -191,6 +187,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
@@ -202,12 +199,15 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
],
),
const VSpace(8),
// favorites
BottomSheetActionWidget(
svg: isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
//TODO(yijing): switch to theme color
iconColor: isFavorite ? Colors.yellow : null,
text: isFavorite
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
@@ -217,6 +217,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
: MobileViewBottomSheetBodyAction.addToFavorites,
),
),
const VSpace(8),
// help center
BottomSheetActionWidget(
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.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/view/view_bloc.dart';
@@ -1,7 +1,4 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -10,6 +7,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({
required BuildContext context,
required WidgetBuilder builder,
}) async {
showModalBottomSheet(
context: context,
isScrollControlled: true,
enableDrag: true,
useSafeArea: true,
builder: builder,
);
}
enum MobileBottomSheetType {
view,
rename,
@@ -42,21 +52,18 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handler
const MobileBottomSheetDragHandler(),
// header
_buildHeader(),
const VSpace(8.0),
const Divider(),
// body
_buildBody(),
const VSpace(24.0),
],
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
),
);
}
@@ -90,23 +97,24 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
context.pop();
break;
}
},
@@ -1,49 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileErrorPage extends StatelessWidget {
const MobileErrorPage({
super.key,
this.header,
this.title,
required this.message,
});
final Widget? header;
final String? title;
final String message;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
header != null
? header!
: const FlowyText.semibold(
'😔',
fontSize: 50,
),
const VSpace(14.0),
FlowyText.semibold(
title ?? LocaleKeys.error_weAreSorry.tr(),
fontSize: 32,
textAlign: TextAlign.center,
),
const VSpace(4.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: FlowyText.regular(
message,
fontSize: 16,
maxLines: 100,
color: Colors.grey, // FIXME: use theme color
textAlign: TextAlign.center,
),
),
],
);
}
}
@@ -1,7 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/error/error_page.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -29,7 +29,7 @@ class MobileFavoritePageFolder extends StatelessWidget {
BlocProvider(
create: (_) => MenuBloc(
user: userProfile,
workspace: workspaceSetting.workspace,
workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
),
BlocProvider(
@@ -49,27 +49,27 @@ class MobileFavoritePageFolder extends StatelessWidget {
builder: (context) {
final favoriteState = context.watch<FavoriteBloc>().state;
if (favoriteState.views.isEmpty) {
return MobileErrorPage(
header: const FlowyText.semibold(
'😁',
fontSize: 50,
),
return FlowyMobileStateContainer.info(
emoji: '😁',
title: LocaleKeys.favorite_noFavorite.tr(),
message: LocaleKeys.favorite_noFavoriteHintText.tr(),
description: LocaleKeys.favorite_noFavoriteHintText.tr(),
);
}
return Scrollbar(
child: SingleChildScrollView(
child: SlidableAutoCloseBehavior(
child: Column(
children: [
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
),
const VSpace(100.0),
],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SlidableAutoCloseBehavior(
child: Column(
children: [
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
),
const VSpace(100.0),
],
),
),
),
),
@@ -19,7 +19,7 @@ class MobileFavoriteScreen extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
FolderEventGetCurrentWorkspace().send(),
FolderEventGetCurrentWorkspaceSetting().send(),
getIt<AuthService>().getUser(),
]),
builder: (context, snapshots) {
@@ -82,12 +82,9 @@ class MobileFavoritePage extends StatelessWidget {
// Folder
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: MobileFavoritePageFolder(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
),
child: MobileFavoritePageFolder(
userProfile: userProfile,
workspaceSetting: workspaceSetting,
),
),
],
@@ -29,7 +29,7 @@ class MobileFolders extends StatelessWidget {
BlocProvider(
create: (_) => MenuBloc(
user: user,
workspace: workspaceSetting.workspace,
workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
),
BlocProvider(
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_recent_files.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
@@ -24,7 +24,7 @@ class MobileHomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
FolderEventGetCurrentWorkspace().send(),
FolderEventGetCurrentWorkspaceSetting().send(),
getIt<AuthService>().getUser(),
]),
builder: (context, snapshots) {
@@ -97,8 +97,7 @@ class MobileHomePage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
// Recent files
const MobileHomePageRecentFilesWidget(),
const Divider(),
const MobileRecentFolder(),
// Folders
Padding(
@@ -110,7 +109,10 @@ class MobileHomePage extends StatelessWidget {
),
),
const SizedBox(height: 8),
const _TrashButton(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: _TrashButton(),
),
],
),
),
@@ -142,7 +144,10 @@ class _TrashButton extends StatelessWidget {
LocaleKeys.trash_text.tr(),
style: Theme.of(context).textTheme.labelMedium,
),
style: const ButtonStyle(alignment: Alignment.centerLeft),
style: const ButtonStyle(
alignment: Alignment.centerLeft,
splashFactory: NoSplash.splashFactory,
),
),
);
}
@@ -1,129 +0,0 @@
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// TODO(yijing): replace by real data later
class MockRecentFile {
MockRecentFile({
required this.title,
});
final String title;
final String icon = '🐼';
final image = Image.asset(
'assets/images/app_flowy_abstract_cover_1.jpg',
fit: BoxFit.cover,
);
}
final recentFilesList = <MockRecentFile>[
MockRecentFile(title: 'Work out plan'),
MockRecentFile(title: 'Travel plan'),
MockRecentFile(title: 'Meeting notes'),
MockRecentFile(title: 'Recipes'),
MockRecentFile(title: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
];
class MobileHomePageRecentFilesWidget extends StatelessWidget {
const MobileHomePageRecentFilesWidget({super.key});
@override
Widget build(BuildContext context) {
// TODO: implement the details later.
return SizedBox(
height: 168,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: FlowyText.semibold(
'Recent',
fontSize: 20.0,
),
),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) => const HSpace(8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
scrollDirection: Axis.horizontal,
itemCount: recentFilesList.length,
itemBuilder: (context, index) {
return Container(
width: 120,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.5),
),
),
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: SizedBox(
height: 60,
width: double.infinity,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: recentFilesList[index].image,
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Container(
height: 32,
width: 32,
margin: const EdgeInsets.only(left: 8),
child: Text(
recentFilesList[index].icon,
style: const TextStyle(fontSize: 32),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 32,
width: double.infinity,
margin: const EdgeInsets.only(
left: 8,
right: 8,
bottom: 8,
),
child: Text(
recentFilesList[index].title,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context)
.colorScheme
.onBackground,
),
maxLines: 2,
),
),
),
],
),
);
},
),
),
],
),
);
}
}
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -23,33 +23,47 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
return FutureBuilder(
future: getIt<AuthService>().getUser(),
builder: ((context, snapshot) {
String? errorMsg;
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
}
final userProfile = snapshot.data?.fold((error) => null, (userProfile) {
final userProfile = snapshot.data?.fold((error) {
errorMsg = error.msg;
return null;
}, (userProfile) {
return userProfile;
});
return Scaffold(
appBar: AppBar(
title: Text(LocaleKeys.settings_title.tr()),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
PersonalInfoSettingGroup(
userProfile: userProfile,
body: userProfile == null
? FlowyMobileStateContainer.error(
emoji: '🛸',
title: LocaleKeys.settings_mobile_userprofileError.tr(),
description: LocaleKeys
.settings_mobile_userprofileErrorDescription
.tr(),
errorMsg: errorMsg,
)
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
PersonalInfoSettingGroup(
userProfile: userProfile,
),
// TODO(yijing): implement this along with Notification Page
const NotificationsSettingGroup(),
const AppearanceSettingGroup(),
const SupportSettingGroup(),
const AboutSettingGroup(),
],
),
),
// TODO(yijing): implement this along with Notification Page
const NotificationsSettingGroup(),
const AppearanceSettingGroup(),
const SupportSettingGroup(),
const AboutSettingGroup(),
],
),
),
),
),
);
}),
);
@@ -1,12 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/trash/application/prelude.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
class MobileHomeTrashPage extends StatelessWidget {
@@ -18,61 +19,53 @@ class MobileHomeTrashPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<TrashBloc>()..add(const TrashEvent.initial()),
child: Builder(
builder: (context) {
child: BlocBuilder<TrashBloc, TrashState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(LocaleKeys.trash_text.tr()),
elevation: 0,
actions: [
IconButton(
splashRadius: 20,
icon: const Icon(Icons.more_horiz),
onPressed: () {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.trash_mobile_actions.tr(),
builder: (_) => Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_restore_m,
text: LocaleKeys.trash_restoreAll.tr(),
onTap: () {
context
..read<TrashBloc>()
.add(const TrashEvent.restoreAll())
..pop();
},
state.objects.isEmpty
? const SizedBox.shrink()
: IconButton(
splashRadius: 20,
icon: const Icon(Icons.more_horiz),
onPressed: () {
final trashBloc = context.read<TrashBloc>();
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.trash_mobile_actions.tr(),
builder: (_) => Row(
children: [
Expanded(
child: _TrashActionAllButton(
trashBloc: trashBloc,
type: _TrashActionType.deleteAll,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: _TrashActionAllButton(
trashBloc: trashBloc,
type: _TrashActionType.restoreAll,
),
)
],
),
),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
text: LocaleKeys.trash_deleteAll.tr(),
onTap: () {
context
..read<TrashBloc>()
.add(const TrashEvent.deleteAll())
..pop();
},
),
)
],
);
},
),
);
},
),
],
),
body: BlocBuilder<TrashBloc, TrashState>(
builder: (_, state) {
if (state.objects.isEmpty) {
return const _TrashEmptyPage();
}
return _DeletedFilesListView(state);
},
),
body: state.objects.isEmpty
? FlowyMobileStateContainer.info(
emoji: '🗑️',
title: LocaleKeys.trash_mobile_empty.tr(),
description: LocaleKeys.trash_mobile_emptyDescription.tr(),
)
: _DeletedFilesListView(state),
);
},
),
@@ -80,6 +73,76 @@ class MobileHomeTrashPage extends StatelessWidget {
}
}
enum _TrashActionType {
restoreAll,
deleteAll,
}
class _TrashActionAllButton extends StatelessWidget {
/// Switch between 'delete all' and 'restore all' feature
const _TrashActionAllButton({
this.type = _TrashActionType.deleteAll,
required this.trashBloc,
});
final _TrashActionType type;
final TrashBloc trashBloc;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDeleteAll = type == _TrashActionType.deleteAll;
return BlocProvider.value(
value: trashBloc,
child: BottomSheetActionWidget(
svg: isDeleteAll ? FlowySvgs.m_delete_m : FlowySvgs.m_restore_m,
text: isDeleteAll
? LocaleKeys.trash_deleteAll.tr()
: LocaleKeys.trash_restoreAll.tr(),
onTap: () {
final trashList = trashBloc.state.objects;
if (trashList.isNotEmpty) {
context.pop();
showFlowyMobileConfirmDialog(
context,
title: isDeleteAll
? LocaleKeys.trash_confirmDeleteAll_title.tr()
: LocaleKeys.trash_restoreAll.tr(),
content: isDeleteAll
? LocaleKeys.trash_confirmDeleteAll_caption.tr()
: LocaleKeys.trash_confirmRestoreAll_caption.tr(),
actionButtonTitle: isDeleteAll
? LocaleKeys.trash_deleteAll.tr()
: LocaleKeys.trash_restoreAll.tr(),
actionButtonColor: isDeleteAll
? theme.colorScheme.error
: theme.colorScheme.primary,
onActionButtonPressed: () {
if (isDeleteAll) {
trashBloc.add(
const TrashEvent.deleteAll(),
);
} else {
trashBloc.add(
const TrashEvent.restoreAll(),
);
}
},
cancelButtonTitle: LocaleKeys.button_cancel.tr(),
);
} else {
// when there is no deleted files
// show toast
Fluttertoast.showToast(
msg: LocaleKeys.trash_mobile_empty.tr(),
gravity: ToastGravity.CENTER,
);
}
},
),
);
}
}
class _DeletedFilesListView extends StatelessWidget {
const _DeletedFilesListView(
this.state,
@@ -89,95 +152,75 @@ class _DeletedFilesListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListView.builder(
itemBuilder: (context, index) {
final object = state.objects[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
// TODO(Yijing): implement file type after TrashPB has file type
leading: FlowySvg(
FlowySvgs.documents_s,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
title: Text(
object.name,
style: theme.textTheme.labelMedium
?.copyWith(color: theme.colorScheme.onBackground),
),
horizontalTitleGap: 0,
// TODO(yiing): needs improve by container/surface theme color
tileColor: theme.colorScheme.onSurface.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO(yijing): extract icon button
IconButton(
splashRadius: 20,
icon: FlowySvg(
FlowySvgs.m_restore_m,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
itemBuilder: (context, index) {
final object = state.objects[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
// TODO(Yijing): implement file type after TrashPB has file type
leading: FlowySvg(
FlowySvgs.documents_s,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
title: Text(
object.name,
style: theme.textTheme.labelMedium
?.copyWith(color: theme.colorScheme.onBackground),
),
horizontalTitleGap: 0,
// TODO(yiing): needs improve by container/surface theme color
tileColor: theme.colorScheme.onSurface.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO(yijing): extract icon button
IconButton(
splashRadius: 20,
icon: FlowySvg(
FlowySvgs.m_restore_m,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
onPressed: () {
context
.read<TrashBloc>()
.add(TrashEvent.putback(object.id));
Fluttertoast.showToast(
msg:
'${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
gravity: ToastGravity.BOTTOM,
);
},
),
onPressed: () {
context
.read<TrashBloc>()
.add(TrashEvent.putback(object.id));
},
),
IconButton(
splashRadius: 20,
icon: FlowySvg(
FlowySvgs.m_delete_m,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
onPressed: () {
context.read<TrashBloc>().add(TrashEvent.delete(object));
},
)
],
IconButton(
splashRadius: 20,
icon: FlowySvg(
FlowySvgs.m_delete_m,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
onPressed: () {
context.read<TrashBloc>().add(TrashEvent.delete(object));
Fluttertoast.showToast(
msg:
'${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
gravity: ToastGravity.BOTTOM,
);
},
)
],
),
),
),
);
},
itemCount: state.objects.length,
);
}
}
class _TrashEmptyPage extends StatelessWidget {
const _TrashEmptyPage();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'🗑️',
style: TextStyle(fontSize: 40),
),
const SizedBox(height: 8),
Text(
LocaleKeys.trash_mobile_empty.tr(),
style: theme.textTheme.labelLarge,
),
const SizedBox(height: 4),
Text(
LocaleKeys.trash_mobile_emptyDescription.tr(),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
),
),
],
);
},
itemCount: state.objects.length,
),
);
}
@@ -0,0 +1,104 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class MobileRecentFolder extends StatefulWidget {
const MobileRecentFolder({super.key});
@override
State<MobileRecentFolder> createState() => _MobileRecentFolderState();
}
class _MobileRecentFolderState extends State<MobileRecentFolder> {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: getIt<MobileRouterRecord>().lastPushedRouter,
builder: (context, value, child) {
return FutureBuilder<Either<RepeatedViewPB, FlowyError>>(
future: FolderEventReadRecentViews().send(),
builder: (context, snapshot) {
final recentViews = snapshot.data
?.fold<List<ViewPB>>(
(l) => l.items,
(r) => [],
)
// only keep the first 10 items.
.reversed
.take(10)
.toList();
if (recentViews == null || recentViews.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0)
],
);
},
);
},
);
}
}
class _RecentViews extends StatelessWidget {
const _RecentViews({
super.key,
required this.recentViews,
});
final List<ViewPB> recentViews;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 168,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FlowyText.semibold(
LocaleKeys.sideBar_recent.tr(),
fontSize: 20.0,
),
),
Expanded(
child: ListView.separated(
separatorBuilder: (context, index) => const HSpace(8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
scrollDirection: Axis.horizontal,
itemCount: recentViews.length,
itemBuilder: (context, index) {
return MobileRecentView(
view: recentViews[index],
height: 120,
);
},
),
),
],
),
);
}
}
@@ -0,0 +1,208 @@
import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:string_validator/string_validator.dart';
class MobileRecentView extends StatefulWidget {
const MobileRecentView({
super.key,
required this.view,
required this.height,
});
final ViewPB view;
final double height;
@override
State<MobileRecentView> createState() => _MobileRecentViewState();
}
class _MobileRecentViewState extends State<MobileRecentView> {
late final ViewListener viewListener;
late ViewPB view;
late final DocumentListener documentListener;
@override
void initState() {
super.initState();
view = widget.view;
viewListener = ViewListener(
viewId: view.id,
)..start(
onViewUpdated: (view) {
setState(() {
this.view = view;
});
},
);
documentListener = DocumentListener(id: view.id)
..start(
didReceiveUpdate: (document) {
setState(() {
view = view;
});
},
);
}
@override
void dispose() {
viewListener.stop();
documentListener.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
final icon = view.icon.value;
final theme = Theme.of(context);
return GestureDetector(
onTap: () => context.pushView(view),
child: Container(
height: widget.height,
width: widget.height,
decoration: BoxDecoration(
color: theme.colorScheme.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.5),
),
),
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: SizedBox(
height: widget.height / 2.0,
width: double.infinity,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: _buildCoverWidget(),
),
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: icon.isNotEmpty
? FlowyText(
icon,
fontSize: 30.0,
)
: SizedBox.square(
dimension: 32.0,
child: view.defaultIcon(),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: widget.height / 2.0,
width: double.infinity,
padding: const EdgeInsets.only(
left: 8.0,
top: 14.0,
right: 8.0,
),
child: FlowyText(
view.name,
maxLines: 2,
fontSize: 16.0,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
);
}
Widget _buildCoverWidget() {
return FutureBuilder<Node?>(
future: _getPageNode(),
builder: ((context, snapshot) {
final node = snapshot.data;
final placeholder = Container(
color: Theme.of(context).colorScheme.onSecondaryContainer,
);
if (node == null) {
return placeholder;
}
final type = CoverType.fromString(
node.attributes[DocumentHeaderBlockKeys.coverType],
);
final cover =
node.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
if (cover == null) {
return placeholder;
}
switch (type) {
case CoverType.file:
if (isURL(cover)) {
return CachedNetworkImage(
imageUrl: cover,
fit: BoxFit.cover,
);
}
final imageFile = File(cover);
if (!imageFile.existsSync()) {
return placeholder;
}
return Image.file(
imageFile,
);
case CoverType.asset:
return Image.asset(
cover,
fit: BoxFit.cover,
);
case CoverType.color:
final color = cover.tryToColor() ?? Colors.white;
return Container(
color: color,
);
case CoverType.none:
return placeholder;
}
}),
);
}
Future<Node?> _getPageNode() async {
final data = await DocumentEventGetDocumentData(
OpenDocumentPayloadPB(documentId: view.id),
).send();
final document = data.fold((l) => l.toDocument(), (r) => null);
if (document != null) {
return document.root;
}
return null;
}
}
@@ -1,8 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -303,11 +303,8 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
_buildLeftIcon(),
const HSpace(4),
// icon
SizedBox.square(
dimension: 22,
child: widget.view.defaultIcon(),
),
const HSpace(12),
_buildViewIconButton(),
const HSpace(8),
// title
Expanded(
child: FlowyText.regular(
@@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
return child;
}
Widget _buildViewIconButton() {
final icon = widget.view.icon.value.isNotEmpty
? FlowyText(
widget.view.icon.value,
fontSize: 24.0,
)
: SizedBox.square(
dimension: 26.0,
child: widget.view.defaultIcon(),
);
return icon;
}
// > button or · button
// show > if the view is expandable.
// show · if the view can't contain child views.
@@ -385,20 +395,24 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
Widget _buildViewAddButton(BuildContext context) {
return MobileViewAddButton(
onPressed: () {
showMobileBottomSheet(
context: context,
builder: (_) => AddNewPageWidgetBottomSheet(
view: widget.view,
onAction: (layout) {
context.pop();
context.read<ViewBloc>().add(
ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
),
);
},
),
final title = widget.view.name;
showFlowyMobileBottomSheet(
context,
title: title,
builder: (_) {
return AddNewPageWidgetBottomSheet(
view: widget.view,
onAction: (layout) {
context.pop();
context.read<ViewBloc>().add(
ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
),
);
},
);
},
);
},
);
@@ -16,7 +16,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
required this.userProfile,
});
final UserProfilePB? userProfile;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
@@ -33,9 +33,9 @@ class PersonalInfoSettingGroup extends StatelessWidget {
settingItemList: [
MobileSettingItem(
name: userName,
subtitle: isCloudEnabled && userProfile != null
subtitle: isCloudEnabled
? Text(
userProfile!.email,
userProfile.email,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
@@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'widgets/widgets.dart';
@@ -14,32 +15,33 @@ class SupportSettingGroup extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_mobile_support.tr(),
settingItemList: [
// 'Help Center'
MobileSettingItem(
name: LocaleKeys.settings_mobile_joinDiscord.tr(),
trailing: const Icon(
Icons.chevron_right,
return FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) => MobileSettingGroup(
groupTitle: LocaleKeys.settings_mobile_support.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_mobile_joinDiscord.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
),
onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
),
MobileSettingItem(
name: LocaleKeys.workspace_errorActions_reportIssue.tr(),
trailing: const Icon(
Icons.chevron_right,
MobileSettingItem(
name: LocaleKeys.workspace_errorActions_reportIssue.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () {
final String? version = snapshot.data?.version;
final String os = Platform.operatingSystem;
safeLaunchUrl(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os',
);
},
),
onTap: () {
// TODO(yijing): get app version before release
const String version = 'Beta';
final String os = Platform.operatingSystem;
safeLaunchUrl(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os',
);
},
),
],
],
),
);
}
}
@@ -0,0 +1,102 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
enum _FlowyMobileStateContainerType {
info,
error,
}
/// Used to display info(like empty state) or error state
/// error state has two buttons to report issue with error message or reach out on discord
class FlowyMobileStateContainer extends StatelessWidget {
const FlowyMobileStateContainer.error({
this.emoji,
required this.title,
this.description,
required this.errorMsg,
super.key,
}) : _stateType = _FlowyMobileStateContainerType.error;
const FlowyMobileStateContainer.info({
this.emoji,
required this.title,
this.description,
super.key,
}) : errorMsg = null,
_stateType = _FlowyMobileStateContainerType.info;
final String? emoji;
final String title;
final String? description;
final String? errorMsg;
final _FlowyMobileStateContainerType _stateType;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox.expand(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
emoji ?? '',
style: const TextStyle(fontSize: 40),
),
const SizedBox(height: 8),
Text(
title,
style: theme.textTheme.labelLarge,
),
const SizedBox(height: 4),
Text(
description ?? '',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
),
),
if (_stateType == _FlowyMobileStateContainerType.error) ...[
const SizedBox(height: 8),
FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedButton(
onPressed: () {
final String? version = snapshot.data?.version;
final String os = Platform.operatingSystem;
safeLaunchUrl(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os&context=Error%20log:%20$errorMsg',
);
},
child: Text(
LocaleKeys.workspace_errorActions_reportIssue.tr(),
),
),
OutlinedButton(
onPressed: () =>
safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
child: Text(
LocaleKeys.workspace_errorActions_reachOut.tr(),
),
),
],
);
},
)
]
],
),
),
);
}
}
@@ -8,6 +8,7 @@ Future<T?> showFlowyMobileBottomSheet<T>(
}) async {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
@@ -0,0 +1,62 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
///show the dialog to confirm one single action
///[onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog
Future<T?> showFlowyMobileConfirmDialog<T>(
BuildContext context, {
String? title,
String? content,
required String actionButtonTitle,
Color? actionButtonColor,
String? cancelButtonTitle,
required void Function()? onActionButtonPressed,
void Function()? onCancelButtonPressed,
}) async {
return showDialog(
context: context,
builder: (dialogContext) {
final foregroundColor = Theme.of(context).colorScheme.onSurface;
return AlertDialog(
title: Text(
title ?? "",
),
content: Text(
content ?? "",
),
actions: [
TextButton(
child: Text(
actionButtonTitle,
style: TextStyle(
color: actionButtonColor ?? foregroundColor,
),
),
onPressed: () {
onActionButtonPressed?.call();
// we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog.
Navigator.of(
dialogContext,
).pop();
},
),
TextButton(
child: Text(
cancelButtonTitle ?? LocaleKeys.button_cancel.tr(),
style: TextStyle(
color: foregroundColor,
),
),
onPressed: () {
onCancelButtonPressed?.call();
Navigator.of(
dialogContext,
).pop();
},
),
],
);
},
);
}
@@ -0,0 +1,3 @@
export 'show_flowy_mobile_confirm_dialog.dart';
export 'show_flowy_mobile_bottom_sheet.dart';
export 'flowy_mobile_state_container.dart';
@@ -0,0 +1,95 @@
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:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// use a global value to store the selected emoji to prevent reloading every time.
EmojiData? _cachedEmojiData;
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
super.key,
required this.onEmojiSelected,
});
final EmojiSelectedCallback onEmojiSelected;
@override
State<FlowyEmojiPicker> createState() => _FlowyEmojiPickerState();
}
class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
EmojiData? emojiData;
@override
void initState() {
super.initState();
// load the emoji data from cache if it's available
if (_cachedEmojiData != null) {
emojiData = _cachedEmojiData;
} else {
EmojiData.builtIn().then(
(value) {
_cachedEmojiData = value;
setState(() {
emojiData = value;
});
},
);
}
}
@override
Widget build(BuildContext context) {
if (emojiData == null) {
return const Center(
child: SizedBox.square(
dimension: 24.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
),
);
}
return EmojiPicker(
emojiData: emojiData!,
configuration: EmojiPickerConfiguration(
showSectionHeader: true,
showTabs: false,
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
),
onEmojiSelected: widget.onEmojiSelected,
headerBuilder: (context, category) {
return FlowyEmojiHeader(
category: category,
);
},
itemBuilder: (context, emojiId, emoji, callback) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(2.0),
icon: FlowyText(
emoji,
fontSize: 28.0,
),
onPressed: () => callback(emojiId, emoji),
);
},
searchBarBuilder: (context, keyword, skinTone) {
return FlowyEmojiSearchBar(
emojiData: emojiData!,
onKeywordChanged: (value) {
keyword.value = value;
},
onSkinToneChanged: (value) {
skinTone.value = value;
},
onRandomEmojiSelected: widget.onEmojiSelected,
);
},
);
}
}
@@ -0,0 +1,48 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FlowyEmojiHeader extends StatelessWidget {
const FlowyEmojiHeader({
super.key,
required this.category,
});
final Category category;
@override
Widget build(BuildContext context) {
if (PlatformExtension.isDesktopOrWeb) {
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
color: Theme.of(context).cardColor,
child: FlowyText.regular(category.id),
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 40,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
color: Theme.of(context).cardColor,
child: Padding(
padding: const EdgeInsets.only(
top: 14.0,
bottom: 4.0,
),
child: FlowyText.regular(category.id),
),
),
const Divider(
height: 1,
thickness: 1,
),
],
);
}
}
}
@@ -0,0 +1,41 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
class FlowyEmojiPickerI18n extends EmojiPickerI18n {
@override
String get activity => LocaleKeys.emoji_categories_activities.tr();
@override
String get flags => LocaleKeys.emoji_categories_flags.tr();
@override
String get foods => LocaleKeys.emoji_categories_food.tr();
@override
String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr();
@override
String get nature => LocaleKeys.emoji_categories_nature.tr();
@override
String get objects => LocaleKeys.emoji_categories_objects.tr();
@override
String get people => LocaleKeys.emoji_categories_smileys.tr();
@override
String get places => LocaleKeys.emoji_categories_places.tr();
@override
String get search => LocaleKeys.emoji_search.tr();
@override
String get symbols => LocaleKeys.emoji_categories_symbols.tr();
@override
String get searchHintText => LocaleKeys.emoji_search.tr();
@override
String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr();
}
@@ -0,0 +1,21 @@
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/base/icon/icon_picker_page.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileEmojiPickerScreen extends StatelessWidget {
static const routeName = '/emoji_picker';
const MobileEmojiPickerScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return IconPickerPage(
onSelected: (result) {
context.pop<EmojiPickerResult>(result);
},
);
}
}
@@ -0,0 +1,156 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
typedef EmojiKeywordChangedCallback = void Function(String keyword);
typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);
class FlowyEmojiSearchBar extends StatefulWidget {
const FlowyEmojiSearchBar({
super.key,
required this.emojiData,
required this.onKeywordChanged,
required this.onSkinToneChanged,
required this.onRandomEmojiSelected,
});
final EmojiData emojiData;
final EmojiKeywordChangedCallback onKeywordChanged;
final EmojiSkinToneChanged onSkinToneChanged;
final EmojiSelectedCallback onRandomEmojiSelected;
@override
State<FlowyEmojiSearchBar> createState() => _FlowyEmojiSearchBarState();
}
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
final TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0,
),
child: Row(
children: [
Expanded(
child: _SearchTextField(
onKeywordChanged: widget.onKeywordChanged,
),
),
const HSpace(6.0),
_RandomEmojiButton(
emojiData: widget.emojiData,
onRandomEmojiSelected: widget.onRandomEmojiSelected,
),
const HSpace(6.0),
FlowyEmojiSkinToneSelector(
onEmojiSkinToneChanged: widget.onSkinToneChanged,
),
const HSpace(6.0),
],
),
);
}
}
class _RandomEmojiButton extends StatelessWidget {
const _RandomEmojiButton({
required this.emojiData,
required this.onRandomEmojiSelected,
});
final EmojiData emojiData;
final EmojiSelectedCallback onRandomEmojiSelected;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.emoji_random.tr(),
child: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.shuffle_rounded,
),
onTap: () {
final random = emojiData.random;
onRandomEmojiSelected(
random.$1,
random.$2,
);
},
),
);
}
}
class _SearchTextField extends StatefulWidget {
const _SearchTextField({
required this.onKeywordChanged,
});
final EmojiKeywordChangedCallback onKeywordChanged;
@override
State<_SearchTextField> createState() => _SearchTextFieldState();
}
class _SearchTextFieldState extends State<_SearchTextField> {
final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 32.0,
),
child: FlowyTextField(
autoFocus: true,
hintText: LocaleKeys.emoji_search.tr(),
controller: controller,
onChanged: widget.onKeywordChanged,
prefixIcon: const Padding(
padding: EdgeInsets.only(
left: 8.0,
right: 4.0,
),
child: FlowySvg(
FlowySvgs.search_s,
),
),
prefixIconConstraints: const BoxConstraints(
maxHeight: 18.0,
),
suffixIcon: Padding(
padding: const EdgeInsets.all(4.0),
child: FlowyButton(
text: const FlowySvg(
FlowySvgs.close_lg,
),
margin: EdgeInsets.zero,
useIntrinsicWidth: true,
onTap: () {
controller.clear();
widget.onKeywordChanged('');
},
),
),
),
);
}
}
@@ -0,0 +1,102 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
// use a temporary global value to store last selected skin tone
EmojiSkinTone? lastSelectedEmojiSkinTone;
@visibleForTesting
ValueKey emojiSkinToneKey(String icon) {
return ValueKey('emoji_skin_tone_$icon');
}
class FlowyEmojiSkinToneSelector extends StatefulWidget {
const FlowyEmojiSkinToneSelector({
super.key,
required this.onEmojiSkinToneChanged,
});
final EmojiSkinToneChanged onEmojiSkinToneChanged;
@override
State<FlowyEmojiSkinToneSelector> createState() =>
_FlowyEmojiSkinToneSelectorState();
}
class _FlowyEmojiSkinToneSelectorState
extends State<FlowyEmojiSkinToneSelector> {
EmojiSkinTone skinTone = EmojiSkinTone.none;
final controller = PopoverController();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: controller,
popupBuilder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: EmojiSkinTone.values
.map(
(e) => _buildIconButton(
e.icon,
() {
setState(() => lastSelectedEmojiSkinTone = e);
widget.onEmojiSkinToneChanged(e);
controller.close();
},
),
)
.toList(),
);
},
child: FlowyTooltip(
message: LocaleKeys.emoji_selectSkinTone.tr(),
child: _buildIconButton(
lastSelectedEmojiSkinTone?.icon ?? '✋',
() => controller.show(),
),
),
);
}
Widget _buildIconButton(String icon, VoidCallback onPressed) {
return FlowyIconButton(
key: emojiSkinToneKey(icon),
icon: Padding(
// add a left padding to align the emoji center
padding: const EdgeInsets.only(
left: 3.0,
),
child: FlowyText(
icon,
fontSize: 22.0,
),
),
onPressed: onPressed,
);
}
}
extension EmojiSkinToneIcon on EmojiSkinTone {
String get icon {
switch (this) {
case EmojiSkinTone.none:
return '✋';
case EmojiSkinTone.light:
return '✋🏻';
case EmojiSkinTone.mediumLight:
return '✋🏼';
case EmojiSkinTone.medium:
return '✋🏽';
case EmojiSkinTone.mediumDark:
return '✋🏾';
case EmojiSkinTone.dark:
return '✋🏿';
}
}
}
@@ -0,0 +1,141 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
enum FlowyIconType {
emoji,
icon,
custom;
}
class EmojiPickerResult {
const EmojiPickerResult(
this.type,
this.emoji,
);
final FlowyIconType type;
final String emoji;
}
class FlowyIconPicker extends StatefulWidget {
const FlowyIconPicker({
super.key,
required this.onSelected,
});
final void Function(EmojiPickerResult result) onSelected;
@override
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
}
class _FlowyIconPickerState extends State<FlowyIconPicker>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
// ONLY supports emoji picker for now
return DefaultTabController(
length: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
_buildTabs(context),
const Spacer(),
_RemoveIconButton(
onTap: () {
widget.onSelected(
const EmojiPickerResult(
FlowyIconType.icon,
'',
),
);
},
),
],
),
const Divider(
height: 2,
),
Expanded(
child: TabBarView(
children: [
FlowyEmojiPicker(
onEmojiSelected: (_, emoji) {
widget.onSelected(
EmojiPickerResult(
FlowyIconType.emoji,
emoji,
),
);
},
),
],
),
),
],
),
);
}
Widget _buildTabs(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
overlayColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.secondary,
),
padding: EdgeInsets.zero,
tabs: [
FlowyHover(
style: const HoverStyle(borderRadius: BorderRadius.zero),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
child: FlowyText(
LocaleKeys.emoji_emojiTab.tr(),
),
),
)
],
),
);
}
}
class _RemoveIconButton extends StatelessWidget {
const _RemoveIconButton({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 28,
child: FlowyButton(
onTap: onTap,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(FlowySvgs.delete_s),
),
);
}
}
@@ -0,0 +1,40 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class IconPickerPage extends StatefulWidget {
const IconPickerPage({
super.key,
required this.onSelected,
});
final void Function(EmojiPickerResult) onSelected;
@override
State<IconPickerPage> createState() => _IconPickerPageState();
}
class _IconPickerPageState extends State<IconPickerPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: const FlowyText.semibold(
'Page icon',
fontSize: 14.0,
),
leading: AppBarBackButton(
onTap: () => context.pop(),
),
),
body: SafeArea(
child: FlowyIconPicker(
onSelected: widget.onSelected,
),
),
);
}
}
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'field_info.dart';
import 'field_service.dart';
part 'field_action_sheet_bloc.freezed.dart';
@@ -14,17 +15,18 @@ class FieldActionSheetBloc
final FieldBackendService fieldService;
final FieldSettingsBackendService fieldSettingsService;
FieldActionSheetBloc({required FieldContext fieldCellContext})
: fieldId = fieldCellContext.fieldInfo.id,
FieldActionSheetBloc({
required String viewId,
required FieldInfo fieldInfo,
}) : fieldId = fieldInfo.id,
fieldService = FieldBackendService(
viewId: fieldCellContext.viewId,
fieldId: fieldCellContext.fieldInfo.id,
viewId: viewId,
fieldId: fieldInfo.id,
),
fieldSettingsService =
FieldSettingsBackendService(viewId: fieldCellContext.viewId),
fieldSettingsService = FieldSettingsBackendService(viewId: viewId),
super(
FieldActionSheetState.initial(
TypeOptionPB.create()..field_2 = fieldCellContext.fieldInfo.field,
TypeOptionPB.create()..field_2 = fieldInfo.field,
),
) {
on<FieldActionSheetEvent>(
@@ -1,77 +1,55 @@
import 'dart:math';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'field_listener.dart';
import 'field_service.dart';
import 'field_info.dart';
part 'field_cell_bloc.freezed.dart';
class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
final SingleFieldListener _fieldListener;
final FieldBackendService _fieldBackendSvc;
FieldInfo fieldInfo;
final FieldSettingsBackendService _fieldSettingsService;
FieldCellBloc({
required FieldContext fieldContext,
}) : _fieldListener =
SingleFieldListener(fieldId: fieldContext.fieldInfo.id),
_fieldBackendSvc = FieldBackendService(
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.id,
FieldCellBloc({required String viewId, required this.fieldInfo})
: _fieldSettingsService = FieldSettingsBackendService(
viewId: viewId,
),
super(FieldCellState.initial(fieldContext)) {
super(FieldCellState.initial(fieldInfo)) {
on<FieldCellEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveFieldUpdate: (field) {
emit(state.copyWith(field: fieldContext.fieldInfo.field));
onFieldChanged: (newFieldInfo) {
fieldInfo = newFieldInfo;
emit(FieldCellState.initial(newFieldInfo));
},
onResizeStart: () {
emit(state.copyWith(resizeStart: state.width));
emit(state.copyWith(isResizing: true, resizeStart: state.width));
},
startUpdateWidth: (offset) {
final width = max(offset + state.resizeStart, 50).toDouble();
emit(state.copyWith(width: width));
},
endUpdateWidth: () {
if (state.width != state.field.width.toDouble()) {
_fieldBackendSvc.updateField(width: state.width);
if (state.width != fieldInfo.fieldSettings?.width.toDouble()) {
_fieldSettingsService.updateFieldSettings(
fieldId: fieldInfo.id,
width: state.width,
);
}
emit(state.copyWith(isResizing: false, resizeStart: 0));
},
);
},
);
}
@override
Future<void> close() async {
await _fieldListener.stop();
return super.close();
}
void _startListening() {
_fieldListener.start(
onFieldChanged: (updatedField) {
if (isClosed) {
return;
}
add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
},
);
}
}
@freezed
class FieldCellEvent with _$FieldCellEvent {
const factory FieldCellEvent.initial() = _InitialCell;
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
const factory FieldCellEvent.onFieldChanged(FieldInfo newFieldInfo) =
_OnFieldChanged;
const factory FieldCellEvent.onResizeStart() = _OnResizeStart;
const factory FieldCellEvent.startUpdateWidth(double offset) =
_StartUpdateWidth;
@@ -81,16 +59,16 @@ class FieldCellEvent with _$FieldCellEvent {
@freezed
class FieldCellState with _$FieldCellState {
const factory FieldCellState({
required String viewId,
required FieldPB field,
required FieldInfo fieldInfo,
required double width,
required bool isResizing,
required double resizeStart,
}) = _FieldCellState;
factory FieldCellState.initial(FieldContext cellContext) => FieldCellState(
viewId: cellContext.viewId,
field: cellContext.fieldInfo.field,
width: cellContext.fieldInfo.field.width.toDouble(),
factory FieldCellState.initial(FieldInfo fieldInfo) => FieldCellState(
fieldInfo: fieldInfo,
isResizing: false,
width: fieldInfo.fieldSettings!.width.toDouble(),
resizeStart: 0,
);
}
@@ -1,12 +1,8 @@
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_service.freezed.dart';
/// FieldService consists of lots of event functions. We define the events in the backend(Rust),
/// you can find the corresponding event implementation in event_map.rs of the corresponding crate.
@@ -104,11 +100,3 @@ class FieldBackendService {
return DatabaseEventGetPrimaryField(payload).send();
}
}
@freezed
class FieldContext with _$FieldContext {
const factory FieldContext({
required String viewId,
required FieldInfo fieldInfo,
}) = _FieldCellContext;
}
@@ -58,6 +58,7 @@ class FieldSettingsBackendService {
Future<Either<Unit, FlowyError>> updateFieldSettings({
required String fieldId,
FieldVisibility? fieldVisibility,
double? width,
}) {
final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create()
..viewId = viewId
@@ -67,6 +68,10 @@ class FieldSettingsBackendService {
payload.visibility = fieldVisibility;
}
if (width != null) {
payload.width = width.round();
}
return DatabaseEventUpdateFieldSettings(payload).send();
}
}
@@ -37,4 +37,15 @@ class GroupBackendService {
}
return DatabaseEventUpdateGroup(payload).send();
}
Future<Either<Unit, FlowyError>> createGroup({
required String name,
String groupConfigId = "",
}) {
final payload = CreateGroupPayloadPB.create()
..viewId = viewId
..name = name;
return DatabaseEventCreateGroup(payload).send();
}
}
@@ -40,6 +40,7 @@ class RowBackendService {
required String rowId,
String? iconURL,
String? coverURL,
bool? isDocumentEmpty,
}) {
final payload = UpdateRowMetaChangesetPB.create()
..viewId = viewId
@@ -52,6 +53,10 @@ class RowBackendService {
payload.coverUrl = coverURL;
}
if (isDocumentEmpty != null) {
payload.isDocumentEmpty = isDocumentEmpty;
}
return DatabaseEventUpdateRowMeta(payload).send();
}
@@ -6,9 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
import 'package:appflowy/plugins/database_view/application/group/group_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@@ -16,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart';
@@ -25,12 +24,13 @@ import 'group_controller.dart';
part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> {
late final GroupBackendService groupBackendSvc;
final DatabaseController databaseController;
late final AppFlowyBoardController boardController;
final LinkedHashMap<String, GroupController> groupControllers =
LinkedHashMap();
GroupPB? ungroupedGroup;
final List<GroupPB> groupList = [];
late final GroupBackendService groupBackendSvc;
late final AppFlowyBoardController boardController;
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
@@ -84,10 +84,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupId: groupId,
startRowId: startRowId,
);
result.fold(
(_) {},
(err) => Log.error(err),
);
result.fold((_) {}, (err) => Log.error(err));
},
createHeaderRow: (String groupId) async {
final result = await databaseController.createRow(
@@ -95,10 +93,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
fromBeginning: true,
);
result.fold(
(_) {},
(err) => Log.error(err),
);
result.fold((_) {}, (err) => Log.error(err));
},
createGroup: (name) async {
final result = await groupBackendSvc.createGroup(name: name);
result.fold((_) {}, (err) => Log.error(err));
},
didCreateRow: (group, row, int? index) {
emit(
@@ -113,6 +112,48 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
_groupItemStartEditing(group, row, true);
},
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
didReceiveGroups: (List<GroupPB> groups) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups);
emit(
state.copyWith(
hiddenGroups: hiddenGroups,
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
didUpdateLayoutSettings: (layoutSettings) {
final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList);
emit(
state.copyWith(
layoutSettings: layoutSettings,
hiddenGroups: hiddenGroups,
),
);
},
toggleGroupVisibility: (GroupPB group, bool isVisible) async {
await _toggleGroupVisibility(group, isVisible);
},
toggleHiddenSectionVisibility: (isVisible) async {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.collapseHiddenGroups = isVisible,
);
await databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
},
reorderGroup: (fromGroupId, toGroupId) async {
_reorderGroup(fromGroupId, toGroupId, emit);
},
startEditingRow: (group, row) {
emit(
state.copyWith(
@@ -138,22 +179,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
emit(state.copyWith(isEditingRow: false, editingRow: null));
}
},
didReceiveGridUpdate: (DatabasePB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
didReceiveGroups: (List<GroupPB> groups) {
emit(
state.copyWith(
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
didUpdateLayoutSettings: (layoutSettings) {
emit(state.copyWith(layoutSettings: layoutSettings));
},
startEditingHeader: (String groupId) {
emit(
state.copyWith(isEditingHeader: true, editingHeaderId: groupId),
@@ -165,7 +190,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupId: groupId,
name: groupName,
);
emit(state.copyWith(isEditingHeader: false));
},
);
@@ -176,13 +200,50 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) {
Log.warn("fieldInfo should not be null");
return;
return Log.warn("fieldInfo should not be null");
}
boardController.enableGroupDragging(!isEdit);
}
Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async {
if (group.isDefault) {
final newLayoutSettings = state.layoutSettings!;
newLayoutSettings.freeze();
final newLayoutSetting = newLayoutSettings.rebuild(
(message) => message.hideUngroupedColumn = !isVisible,
);
return databaseController.updateLayoutSetting(
boardLayoutSetting: newLayoutSetting,
);
}
await groupBackendSvc.updateGroup(
fieldId: groupControllers.values.first.group.fieldId,
groupId: group.groupId,
visible: isVisible,
);
}
Future<void> _reorderGroup(
String fromGroupId,
String toGroupId,
Emitter<BoardState> emit,
) async {
final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId);
final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId);
final group = groupList.removeAt(fromIndex);
groupList.insert(toIndex, group);
add(BoardEvent.didReceiveGroups(groupList));
final result = await databaseController.moveGroup(
fromGroupId: fromGroupId,
toGroupId: toGroupId,
);
result.fold((l) => {}, (err) => Log.error(err));
}
@override
Future<void> close() async {
for (final controller in groupControllers.values) {
@@ -191,40 +252,45 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
return super.close();
}
bool get hideUngrouped =>
databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??
false;
FieldType get groupingFieldType {
final fieldInfo =
databaseController.fieldController.getField(groupList.first.fieldId)!;
return fieldInfo.fieldType;
}
void initializeGroups(List<GroupPB> groups) {
for (final controller in groupControllers.values) {
controller.dispose();
}
groupControllers.clear();
boardController.clear();
final ungroupedGroupIndex =
groups.indexWhere((group) => group.groupId == group.fieldId);
if (ungroupedGroupIndex != -1) {
ungroupedGroup = groups[ungroupedGroupIndex];
final group = groups.removeAt(ungroupedGroupIndex);
if (!(state.layoutSettings?.hideUngroupedColumn ?? false)) {
groups.add(group);
}
}
groupList.clear();
groupList.addAll(groups);
boardController.addGroups(
groups
.where((group) => fieldController.getField(group.fieldId) != null)
.map((group) => initializeGroupData(group))
.where(
(group) =>
fieldController.getField(group.fieldId) != null &&
(group.isVisible || (group.isDefault && !hideUngrouped)),
)
.map((group) => _initializeGroupData(group))
.toList(),
);
for (final group in groups) {
final controller = initializeGroupController(group);
groupControllers[controller.group.groupId] = (controller);
final controller = _initializeGroupController(group);
groupControllers[controller.group.groupId] = controller;
}
}
RowCache? getRowCache() {
return databaseController.rowCache;
}
RowCache? getRowCache() => databaseController.rowCache;
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
@@ -236,15 +302,22 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks(
onLayoutSettingsChanged: (layoutSettings) {
if (isClosed || !layoutSettings.hasBoard()) {
if (isClosed) {
return;
}
if (ungroupedGroup != null) {
final index = groupList.indexWhere((element) => element.isDefault);
if (index != -1) {
if (layoutSettings.board.hideUngroupedColumn) {
boardController.removeGroup(ungroupedGroup!.fieldId);
boardController.removeGroup(groupList[index].fieldId);
} else {
final newGroup = initializeGroupData(ungroupedGroup!);
boardController.addGroup(newGroup);
final newGroup = _initializeGroupData(groupList[index]);
final visibleGroups = [...groupList]
..retainWhere((g) => g.isVisible || g.isDefault);
final indexInVisibleGroups =
visibleGroups.indexWhere((g) => g.isDefault);
if (indexInVisibleGroups != -1) {
boardController.insertGroup(indexInVisibleGroups, newGroup);
}
}
}
add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board));
@@ -252,30 +325,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
final onGroupChanged = GroupCallbacks(
onGroupByField: (groups) {
if (isClosed) return;
ungroupedGroup = null;
if (isClosed) {
return;
}
initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups));
},
onDeleteGroup: (groupIds) {
if (isClosed) return;
if (isClosed) {
return;
}
boardController.removeGroups(groupIds);
groupList.removeWhere((group) => groupIds.contains(group.groupId));
add(BoardEvent.didReceiveGroups(groupList));
},
onInsertGroup: (insertGroups) {
if (isClosed) return;
if (isClosed) {
return;
}
final group = insertGroups.group;
final newGroup = initializeGroupData(group);
final controller = initializeGroupController(group);
groupControllers[controller.group.groupId] = (controller);
final newGroup = _initializeGroupData(group);
final controller = _initializeGroupController(group);
groupControllers[controller.group.groupId] = controller;
boardController.addGroup(newGroup);
groupList.insert(insertGroups.index, group);
add(BoardEvent.didReceiveGroups(groupList));
},
onUpdateGroup: (updatedGroups) {
if (isClosed) return;
if (isClosed) {
return;
}
for (final group in updatedGroups) {
// see if the column is already in the board
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
if (index == -1) continue;
final columnController =
boardController.getGroupController(group.groupId);
columnController?.updateGroupName(group.groupName);
if (columnController != null) {
// remove the group or update its name
columnController.updateGroupName(group.groupName);
if (!group.isVisible) {
boardController.removeGroup(group.groupId);
}
} else {
final newGroup = _initializeGroupData(group);
final visibleGroups = [...groupList]..retainWhere(
(g) =>
g.isVisible ||
g.isDefault && !hideUngrouped ||
g.groupId == group.groupId,
);
final indexInVisibleGroups =
visibleGroups.indexWhere((g) => g.groupId == group.groupId);
if (indexInVisibleGroups != -1) {
boardController.insertGroup(indexInVisibleGroups, newGroup);
}
}
groupList.removeAt(index);
groupList.insert(index, group);
}
add(BoardEvent.didReceiveGroups(groupList));
},
);
@@ -313,24 +428,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
GroupController initializeGroupController(GroupPB group) {
GroupController _initializeGroupController(GroupPB group) {
final delegate = GroupControllerDelegateImpl(
controller: boardController,
fieldController: fieldController,
onNewColumnItem: (groupId, row, index) {
add(BoardEvent.didCreateRow(group, row, index));
},
onNewColumnItem: (groupId, row, index) =>
add(BoardEvent.didCreateRow(group, row, index)),
);
final controller = GroupController(
viewId: state.viewId,
group: group,
delegate: delegate,
onGroupChanged: (newGroup) {
if (isClosed) return;
final index =
groupList.indexWhere((g) => g.groupId == newGroup.groupId);
if (index != -1) {
groupList.removeAt(index);
groupList.insert(index, newGroup);
add(BoardEvent.didReceiveGroups(groupList));
}
},
);
controller.startListening();
return controller;
return controller..startListening();
}
AppFlowyGroupData initializeGroupData(GroupPB group) {
AppFlowyGroupData _initializeGroupData(GroupPB group) {
return AppFlowyGroupData(
id: group.groupId,
name: group.groupName,
@@ -348,6 +474,7 @@ class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = _InitialBoard;
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.createGroup(String name) = _CreateGroup;
const factory BoardEvent.startEditingHeader(String groupId) =
_StartEditingHeader;
const factory BoardEvent.endEditingHeader(String groupId, String groupName) =
@@ -362,6 +489,14 @@ class BoardEvent with _$BoardEvent {
RowMetaPB row,
) = _StartEditRow;
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
const factory BoardEvent.toggleGroupVisibility(
GroupPB group,
bool isVisible,
) = _ToggleGroupVisibility;
const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =
_ToggleHiddenSectionVisibility;
const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =
_ReorderGroup;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveGridUpdate(
DatabasePB grid,
@@ -380,12 +515,13 @@ class BoardState with _$BoardState {
required Option<DatabasePB> grid,
required List<String> groupIds,
required bool isEditingHeader,
String? editingHeaderId,
required bool isEditingRow,
BoardEditingRow? editingRow,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
required BoardLayoutSettingPB? layoutSettings,
String? editingHeaderId,
BoardEditingRow? editingRow,
required List<GroupPB> hiddenGroups,
}) = _BoardState;
factory BoardState.initial(String viewId) => BoardState(
@@ -397,30 +533,14 @@ class BoardState with _$BoardState {
noneOrError: none(),
loadingState: const LoadingState.loading(),
layoutSettings: null,
hiddenGroups: [],
);
}
class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields;
const GridFieldEquatable(
UnmodifiableListView<FieldPB> fields,
) : _fields = fields;
@override
List<Object?> get props {
if (_fields.isEmpty) {
return [];
}
return [
_fields.length,
_fields
.map((field) => field.width)
.reduce((value, element) => value + element),
];
}
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {
return [...groups]..retainWhere(
(group) => !group.isVisible || group.isDefault && hideUngrouped,
);
}
class GroupItem extends AppFlowyGroupItem {
@@ -450,12 +570,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
required this.onNewColumnItem,
});
@override
bool hasGroup(String groupId) {
return controller.groupIds.contains(groupId);
}
@override
void insertRow(GroupPB group, RowMetaPB row, int? index) {
final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) {
Log.warn("fieldInfo should not be null");
return;
return Log.warn("fieldInfo should not be null");
}
if (index != null) {
@@ -474,17 +598,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
}
@override
void removeRow(GroupPB group, RowId rowId) {
controller.removeGroupItem(group.groupId, rowId.toString());
}
void removeRow(GroupPB group, RowId rowId) =>
controller.removeGroupItem(group.groupId, rowId.toString());
@override
void updateRow(GroupPB group, RowMetaPB row) {
final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) {
Log.warn("fieldInfo should not be null");
return;
return Log.warn("fieldInfo should not be null");
}
controller.updateGroupItem(
group.groupId,
GroupItem(
@@ -498,20 +621,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
final fieldInfo = fieldController.getField(group.fieldId);
if (fieldInfo == null) {
Log.warn("fieldInfo should not be null");
return;
return Log.warn("fieldInfo should not be null");
}
final item = GroupItem(
row: row,
fieldInfo: fieldInfo,
draggable: false,
);
final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false);
if (index != null) {
controller.insertGroupItem(group.groupId, index, item);
} else {
controller.addGroupItem(group.groupId, item);
}
onNewColumnItem(group.groupId, row, index);
}
}
@@ -529,27 +649,26 @@ class BoardEditingRow {
}
class GroupData {
final GroupPB group;
final FieldInfo fieldInfo;
GroupData({
required this.group,
required this.fieldInfo,
});
CheckboxGroup? asCheckboxGroup() {
if (fieldType != FieldType.Checkbox) return null;
return CheckboxGroup(group);
}
final GroupPB group;
final FieldInfo fieldInfo;
CheckboxGroup? asCheckboxGroup() =>
fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null;
FieldType get fieldType => fieldInfo.fieldType;
}
class CheckboxGroup {
const CheckboxGroup(this.group);
final GroupPB group;
CheckboxGroup(this.group);
// Hardcode value: "Yes" that equal to the value defined in Rust
// pub const CHECK: &str = "Yes";
// Hardcode value: "Yes" that equal to the value defined in Rust
// pub const CHECK: &str = "Yes";
bool get isCheck => group.groupId == "Yes";
}
@@ -1,12 +0,0 @@
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
class BoardGroupService {
final String viewId;
FieldPB? groupField;
BoardGroupService(this.viewId);
void setGroupField(FieldPB field) {
groupField = field;
}
}
@@ -7,10 +7,12 @@ import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate {
bool hasGroup(String groupId);
void removeRow(GroupPB group, RowId rowId);
void insertRow(GroupPB group, RowMetaPB row, int? index);
void updateRow(GroupPB group, RowMetaPB row);
@@ -18,14 +20,16 @@ abstract class GroupControllerDelegate {
}
class GroupController {
final GroupPB group;
GroupPB group;
final SingleGroupListener _listener;
final GroupControllerDelegate delegate;
final void Function(GroupPB group) onGroupChanged;
GroupController({
required String viewId,
required this.group,
required this.delegate,
required this.onGroupChanged,
}) : _listener = SingleGroupListener(group);
RowMetaPB? rowAtIndex(int index) {
@@ -46,37 +50,52 @@ class GroupController {
onGroupChanged: (result) {
result.fold(
(GroupRowsNotificationPB changeset) {
final newItems = [...group.rows];
final isGroupExist = delegate.hasGroup(group.groupId);
for (final deletedRow in changeset.deletedRows) {
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
delegate.removeRow(group, deletedRow);
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
if (isGroupExist) {
delegate.removeRow(group, deletedRow);
}
}
for (final insertedRow in changeset.insertedRows) {
final index = insertedRow.hasIndex() ? insertedRow.index : null;
if (insertedRow.hasIndex() &&
group.rows.length > insertedRow.index) {
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
newItems.length > insertedRow.index) {
newItems.insert(insertedRow.index, insertedRow.rowMeta);
} else {
group.rows.add(insertedRow.rowMeta);
newItems.add(insertedRow.rowMeta);
}
if (insertedRow.isNew) {
delegate.addNewRow(group, insertedRow.rowMeta, index);
} else {
delegate.insertRow(group, insertedRow.rowMeta, index);
if (isGroupExist) {
if (insertedRow.isNew) {
delegate.addNewRow(group, insertedRow.rowMeta, index);
} else {
delegate.insertRow(group, insertedRow.rowMeta, index);
}
}
}
for (final updatedRow in changeset.updatedRows) {
final index = group.rows.indexWhere(
final index = newItems.indexWhere(
(rowPB) => rowPB.id == updatedRow.id,
);
if (index != -1) {
group.rows[index] = updatedRow;
delegate.updateRow(group, updatedRow);
newItems[index] = updatedRow;
if (isGroupExist) {
delegate.updateRow(group, updatedRow);
}
}
}
group.freeze();
group = group.rebuild((group) {
group.rows.clear();
group.rows.addAll(newItems);
});
onGroupChanged(group);
},
(err) => Log.error(err),
);
@@ -1,112 +0,0 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'group_controller.dart';
part 'ungrouped_items_bloc.freezed.dart';
class UngroupedItemsBloc
extends Bloc<UngroupedItemsEvent, UngroupedItemsState> {
UngroupedItemsListener? listener;
UngroupedItemsBloc({required GroupPB group})
: super(UngroupedItemsState(ungroupedItems: group.rows)) {
on<UngroupedItemsEvent>(
(event, emit) {
event.when(
initial: () {
listener = UngroupedItemsListener(
initialGroup: group,
onGroupChanged: (ungroupedItems) {
if (isClosed) return;
add(
UngroupedItemsEvent.updateGroup(
ungroupedItems: ungroupedItems,
),
);
},
)..startListening();
},
updateGroup: (newItems) =>
emit(UngroupedItemsState(ungroupedItems: newItems)),
);
},
);
}
}
@freezed
class UngroupedItemsEvent with _$UngroupedItemsEvent {
const factory UngroupedItemsEvent.initial() = _Initial;
const factory UngroupedItemsEvent.updateGroup({
required List<RowMetaPB> ungroupedItems,
}) = _UpdateGroup;
}
@freezed
class UngroupedItemsState with _$UngroupedItemsState {
const factory UngroupedItemsState({
required List<RowMetaPB> ungroupedItems,
}) = _UngroupedItemsState;
}
class UngroupedItemsListener {
List<RowMetaPB> _ungroupedItems;
final SingleGroupListener _listener;
final void Function(List<RowMetaPB> items) onGroupChanged;
UngroupedItemsListener({
required GroupPB initialGroup,
required this.onGroupChanged,
}) : _ungroupedItems = List<RowMetaPB>.from(initialGroup.rows),
_listener = SingleGroupListener(initialGroup);
void startListening() {
_listener.start(
onGroupChanged: (result) {
result.fold(
(GroupRowsNotificationPB changeset) {
final newItems = List<RowMetaPB>.from(_ungroupedItems);
for (final deletedRow in changeset.deletedRows) {
newItems.removeWhere((rowPB) => rowPB.id == deletedRow);
}
for (final insertedRow in changeset.insertedRows) {
final index = newItems.indexWhere(
(rowPB) => rowPB.id == insertedRow.rowMeta.id,
);
if (index != -1) {
continue;
}
if (insertedRow.hasIndex() &&
newItems.length > insertedRow.index) {
newItems.insert(insertedRow.index, insertedRow.rowMeta);
} else {
newItems.add(insertedRow.rowMeta);
}
}
for (final updatedRow in changeset.updatedRows) {
final index = newItems.indexWhere(
(rowPB) => rowPB.id == updatedRow.id,
);
if (index != -1) {
newItems[index] = updatedRow;
}
}
onGroupChanged.call(newItems);
_ungroupedItems = newItems;
},
(err) => Log.error(err),
);
},
);
}
Future<void> dispose() async {
_listener.stop();
}
}
@@ -1,5 +1,3 @@
// ignore_for_file: unused_field
import 'dart:collection';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -9,19 +7,19 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../widgets/card/cells/card_cell.dart';
@@ -30,7 +28,7 @@ import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart';
import '../../widgets/card/card.dart';
import 'toolbar/board_setting_bar.dart';
import 'ungrouped_items_button.dart';
import 'widgets/board_hidden_groups.dart';
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override
@@ -39,46 +37,38 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
ViewPB view,
DatabaseController controller,
bool shrinkWrap,
) {
return BoardPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
) =>
BoardPage(view: view, databaseController: controller);
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
}
Widget settingBar(BuildContext context, DatabaseController controller) =>
BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return SizedBox.fromSize();
}
) =>
const SizedBox.shrink();
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
ValueKey _makeValueKey(DatabaseController controller) =>
ValueKey(controller.viewId);
}
class BoardPage extends StatelessWidget {
final DatabaseController databaseController;
BoardPage({
required this.view,
required this.databaseController,
Key? key,
this.onEditStateChanged,
}) : super(key: ValueKey(view.id));
final ViewPB view;
final DatabaseController databaseController;
/// Called when edit state changed
final VoidCallback? onEditStateChanged;
@@ -91,23 +81,18 @@ class BoardPage extends StatelessWidget {
)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) {
return state.loadingState.map(
loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) {
return result.successOrFail.fold(
(_) => BoardContent(
onEditStateChanged: onEditStateChanged,
),
(err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
);
},
);
},
builder: (context, state) => state.loadingState.map(
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
finish: (result) => result.successOrFail.fold(
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
(err) => FlowyErrorPage.message(
err.toString(),
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
),
),
),
),
);
}
@@ -115,9 +100,9 @@ class BoardPage extends StatelessWidget {
class BoardContent extends StatefulWidget {
const BoardContent({
Key? key,
super.key,
this.onEditStateChanged,
}) : super(key: key);
});
final VoidCallback? onEditStateChanged;
@@ -126,11 +111,14 @@ class BoardContent extends StatefulWidget {
}
class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager;
final renderHook = RowCardRenderHook<String>();
late final ScrollController scrollController;
late final AppFlowyBoardScrollController scrollManager;
final config = const AppFlowyBoardConfig(
groupBackgroundColor: Color(0xffF7F8FC),
headerPadding: EdgeInsets.symmetric(horizontal: 8),
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
);
@override
@@ -138,6 +126,7 @@ class _BoardContentState extends State<BoardContent> {
super.initState();
scrollManager = AppFlowyBoardScrollController();
scrollController = ScrollController();
renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId.
final isInGroup =
@@ -160,42 +149,36 @@ class _BoardContentState extends State<BoardContent> {
},
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
return Padding(
padding: GridSize.contentInsets,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(8.0),
if (state.layoutSettings?.hideUngroupedColumn ?? false)
_buildBoardHeader(context),
Expanded(
child: AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: (_, groupData) =>
BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: BoardColumnHeader(
groupData: groupData,
margin: config.headerPadding,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor:
Theme.of(context).colorScheme.surfaceVariant,
),
),
)
],
padding: const EdgeInsets.only(top: 8.0),
child: AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: const AppFlowyBoardConfig(
groupPadding: EdgeInsets.symmetric(horizontal: 4),
groupItemPadding: EdgeInsets.symmetric(horizontal: 4),
),
leading: HiddenGroupsColumn(margin: config.headerPadding),
trailing: showCreateGroupButton
? BoardTrailing(scrollController: scrollController)
: null,
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: BoardColumnHeader(
groupData: groupData,
margin: config.headerPadding,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
),
);
},
@@ -203,19 +186,6 @@ class _BoardContentState extends State<BoardContent> {
);
}
Widget _buildBoardHeader(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: SizedBox(
height: 24,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: UngroupedItemsButton(),
),
),
);
}
void _handleEditStateChanged(BoardState state, BuildContext context) {
if (state.isEditingRow && state.editingRow != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -228,25 +198,24 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
return AppFlowyGroupFooter(
height: 50,
margin: config.footerPadding,
icon: SizedBox(
height: 20,
width: 20,
child: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color,
color: Theme.of(context).hintColor,
),
),
title: FlowyText.medium(
LocaleKeys.board_column_createNewCard.tr(),
color: Theme.of(context).hintColor,
fontSize: 14,
),
height: 50,
margin: config.footerPadding,
onAddButtonClick: () {
context.read<BoardBloc>().add(
BoardEvent.createBottomRow(columnData.id),
);
},
onAddButtonClick: () => context
.read<BoardBloc>()
.add(BoardEvent.createBottomRow(columnData.id)),
);
}
@@ -272,6 +241,7 @@ class _BoardContentState extends State<BoardContent> {
boardBloc.state.editingRow?.row.id == groupItem.row.id;
final groupItemId = groupItem.row.id + groupData.group.groupId;
return AppFlowyGroupCard(
key: ValueKey(groupItemId),
margin: config.cardPadding,
@@ -303,15 +273,27 @@ class _BoardContentState extends State<BoardContent> {
}
BoxDecoration _makeBoxDecoration(BuildContext context) {
final borderSide = BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0,
);
final isLightMode = Theme.of(context).brightness == Brightness.light;
return BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: isLightMode ? Border.fromBorderSide(borderSide) : null,
borderRadius: const BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.4,
),
),
boxShadow: [
BoxShadow(
blurRadius: 4,
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
),
BoxShadow(
blurRadius: 4,
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
),
],
);
}
@@ -339,12 +321,113 @@ class _BoardContentState extends State<BoardContent> {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
rowController: dataController,
builder: (_) => RowDetailPage(
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
rowController: dataController,
),
);
}
}
class BoardTrailing extends StatefulWidget {
const BoardTrailing({super.key, required this.scrollController});
final ScrollController scrollController;
@override
State<BoardTrailing> createState() => _BoardTrailingState();
}
class _BoardTrailingState extends State<BoardTrailing> {
final TextEditingController _textController = TextEditingController();
late final FocusNode _focusNode;
bool isEditing = false;
void _cancelAddNewGroup() {
_textController.clear();
setState(() => isEditing = false);
}
@override
void initState() {
super.initState();
_focusNode = FocusNode(
onKeyEvent: (node, event) {
if (_focusNode.hasFocus &&
event.logicalKey == LogicalKeyboardKey.escape) {
_cancelAddNewGroup();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
)..addListener(() {
if (!_focusNode.hasFocus) {
_cancelAddNewGroup();
}
});
}
@override
Widget build(BuildContext context) {
// call after every setState
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isEditing) {
_focusNode.requestFocus();
widget.scrollController.jumpTo(
widget.scrollController.position.maxScrollExtent,
);
},
}
});
return Padding(
padding: const EdgeInsets.only(left: 8.0, top: 12),
child: Align(
alignment: AlignmentDirectional.topStart,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isEditing
? SizedBox(
width: 256,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
suffixIcon: Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8.0),
child: FlowyIconButton(
icon: const FlowySvg(FlowySvgs.close_filled_m),
hoverColor: Colors.transparent,
onPressed: () => _textController.clear(),
),
),
suffixIconConstraints:
BoxConstraints.loose(const Size(20, 24)),
border: const UnderlineInputBorder(),
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
isDense: true,
),
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
onSubmitted: (groupName) => context
.read<BoardBloc>()
.add(BoardEvent.createGroup(groupName)),
),
),
)
: FlowyTooltip(
message: LocaleKeys.board_column_createNewColumn.tr(),
child: FlowyIconButton(
width: 26,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => setState(() => isEditing = true),
),
),
),
),
);
}
}
@@ -1,236 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/board/application/ungrouped_items_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.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 UngroupedItemsButton extends StatefulWidget {
const UngroupedItemsButton({super.key});
@override
State<UngroupedItemsButton> createState() => _UnscheduledEventsButtonState();
}
class _UnscheduledEventsButtonState extends State<UngroupedItemsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, boardState) {
final ungroupedGroup = context.watch<BoardBloc>().ungroupedGroup;
final databaseController = context.read<BoardBloc>().databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
if (ungroupedGroup == null) {
return const SizedBox.shrink();
}
return BlocProvider<UngroupedItemsBloc>(
create: (_) => UngroupedItemsBloc(group: ungroupedGroup)
..add(const UngroupedItemsEvent.initial()),
child: BlocBuilder<UngroupedItemsBloc, UngroupedItemsState>(
builder: (context, state) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
controller: _popoverController,
offset: const Offset(0, 8),
constraints:
const BoxConstraints(maxWidth: 282, maxHeight: 600),
child: FlowyTooltip(
message: LocaleKeys.board_ungroupedButtonTooltip.tr(),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: Corners.s6Border,
),
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.ungroupedItems.isNotEmpty) {
_popoverController.show();
}
},
child: FlowyText.regular(
"${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})",
fontSize: 10,
),
),
),
popupBuilder: (context) {
return UngroupedItemList(
viewId: databaseController.viewId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
ungroupedItems: state.ungroupedItems,
);
},
);
},
),
);
},
);
}
}
class UngroupedItemList extends StatelessWidget {
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
final List<RowMetaPB> ungroupedItems;
const UngroupedItemList({
required this.viewId,
required this.primaryField,
required this.ungroupedItems,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.board_ungroupedItemsTitle.tr(),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...ungroupedItems.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
);
});
return UngroupedItem(
cellContext: rowCache.loadCells(item)[primaryField.id]!,
primaryField: primaryField,
rowController: rowController,
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
renderHook: renderHook,
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder:
GridCellBuilder(cellCache: rowController.cellCache),
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
},
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UngroupedItem extends StatelessWidget {
final DatabaseCellContext cellContext;
final FieldInfo primaryField;
final RowController rowController;
final CardCellBuilder cellBuilder;
final RowCardRenderHook<String> renderHook;
final VoidCallback onPressed;
const UngroupedItem({
super.key,
required this.cellContext,
required this.onPressed,
required this.cellBuilder,
required this.rowController,
required this.primaryField,
required this.renderHook,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: cellBuilder.buildCell(
cellContext: cellContext,
renderHook: renderHook,
),
onTap: onPressed,
),
);
}
}
@@ -1,14 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/card/define.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
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/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.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/services.dart';
@@ -18,11 +19,11 @@ class BoardColumnHeader extends StatefulWidget {
const BoardColumnHeader({
super.key,
required this.groupData,
this.margin,
required this.margin,
});
final AppFlowyGroupData groupData;
final EdgeInsets? margin;
final EdgeInsets margin;
@override
State<BoardColumnHeader> createState() => _BoardColumnHeaderState();
@@ -74,7 +75,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
child: FlowyText.medium(
widget.groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
overflow: TextOverflow.ellipsis,
),
);
@@ -84,22 +85,16 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
fit: FlexFit.tight,
child: FlowyTooltip(
message: LocaleKeys.board_column_renameGroupTooltip.tr(),
child: FlowyHover(
style: HoverStyle(
hoverColor: Colors.transparent,
foregroundColorOnHover:
AFThemeExtension.of(context).textColor,
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context.read<BoardBloc>().add(
BoardEvent.startEditingHeader(
widget.groupData.id,
),
),
onTap: () => context
.read<BoardBloc>()
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
child: FlowyText.medium(
widget.groupData.headerData.groupName,
fontSize: 14,
overflow: TextOverflow.clip,
overflow: TextOverflow.ellipsis,
),
),
),
@@ -112,22 +107,31 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
title = _buildTextField(context);
}
return AppFlowyGroupHeader(
title: title,
icon: _buildHeaderIcon(boardCustomData),
addIcon: SizedBox(
height: 20,
width: 20,
child: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).iconTheme.color,
return Padding(
padding: widget.margin,
child: SizedBox(
height: 50,
child: Row(
children: [
_buildHeaderIcon(boardCustomData),
title,
const HSpace(6),
_groupOptionsButton(context),
const HSpace(4),
FlowyTooltip(
message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.add_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
),
),
],
),
),
onAddButtonClick: () => context
.read<BoardBloc>()
.add(BoardEvent.createHeaderRow(widget.groupData.id)),
height: 50,
margin: widget.margin ?? EdgeInsets.zero,
);
},
);
@@ -154,7 +158,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
hoverColor: Colors.transparent,
// Magic number 4 makes the textField take up the same space as FlowyText
contentPadding: EdgeInsets.symmetric(
vertical: CardSizes.cardCellVPadding + 4,
horizontal: 8,
@@ -181,45 +184,93 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
);
}
void _saveEdit() {
context.read<BoardBloc>().add(
BoardEvent.endEditingHeader(
widget.groupData.id,
_controller.text,
void _saveEdit() => context
.read<BoardBloc>()
.add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text));
Widget _buildHeaderIcon(GroupData customData) =>
switch (customData.fieldType) {
FieldType.Checkbox => FlowySvg(
customData.asCheckboxGroup()!.isCheck
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
_ => const SizedBox.shrink(),
};
Widget _groupOptionsButton(BuildContext context) {
return AppFlowyPopover(
clickHandler: PopoverClickHandler.gestureDetector,
margin: const EdgeInsets.fromLTRB(8, 8, 8, 4),
constraints: BoxConstraints.loose(const Size(168, 300)),
direction: PopoverDirection.bottomWithLeftAligned,
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.details_horizontal_s),
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
),
popupBuilder: (popoverContext) {
final customGroupData = widget.groupData.customData as GroupData;
final menuItems = GroupOptions.values.toList();
if (!customGroupData.fieldType.canEditHeader) {
menuItems.remove(GroupOptions.rename);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
...menuItems.map(
(action) => SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyButton(
leftIcon: FlowySvg(action.icon),
text: FlowyText.medium(
action.text,
overflow: TextOverflow.ellipsis,
),
onTap: () {
action.call(context, customGroupData.group);
PopoverContainer.of(popoverContext).close();
},
),
),
),
)
],
);
}
}
Widget? _buildHeaderIcon(GroupData customData) {
Widget? widget;
switch (customData.fieldType) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
widget = FlowySvg(
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
break;
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
case FieldType.MultiSelect:
case FieldType.Number:
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.URL:
case FieldType.Checklist:
break;
}
if (widget != null) {
widget = SizedBox(
width: 20,
height: 20,
child: widget,
},
);
}
}
return widget;
enum GroupOptions {
rename,
hide;
void call(BuildContext context, GroupPB group) {
switch (this) {
case rename:
context
.read<BoardBloc>()
.add(BoardEvent.startEditingHeader(group.groupId));
break;
case hide:
context
.read<BoardBloc>()
.add(BoardEvent.toggleGroupVisibility(group, false));
break;
}
}
FlowySvgData get icon => switch (this) {
rename => FlowySvgs.edit_s,
hide => FlowySvgs.hide_s,
};
String get text => switch (this) {
rename => LocaleKeys.board_column_renameColumn.tr(),
hide => LocaleKeys.board_column_hideColumn.tr(),
};
}
@@ -0,0 +1,483 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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_bloc/flutter_bloc.dart';
class HiddenGroupsColumn extends StatelessWidget {
final EdgeInsets margin;
const HiddenGroupsColumn({super.key, required this.margin});
@override
Widget build(BuildContext context) {
final databaseController = context.read<BoardBloc>().databaseController;
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
selector: (state) => state.layoutSettings,
builder: (context, layoutSettings) {
if (layoutSettings == null) {
return const SizedBox.shrink();
}
final isCollapsed = layoutSettings.collapseHiddenGroups;
return AnimatedSize(
alignment: AlignmentDirectional.topStart,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 150),
child: isCollapsed
? SizedBox(
height: 50,
child: Padding(
padding: const EdgeInsets.only(left: 40, right: 8),
child: Center(
child: _collapseExpandIcon(context, isCollapsed),
),
),
)
: SizedBox(
width: 260,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 50,
child: Padding(
padding: EdgeInsets.only(
left: 40 + margin.left,
right: margin.right,
),
child: Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys
.board_hiddenGroupSection_sectionTitle
.tr(),
fontSize: 14,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
_collapseExpandIcon(context, isCollapsed),
],
),
),
),
Expanded(
child: HiddenGroupList(
databaseController: databaseController,
),
),
],
),
),
);
},
);
}
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
return FlowyTooltip(
message: isCollapsed
? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr()
: LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(),
child: FlowyIconButton(
width: 20,
height: 20,
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
icon: FlowySvg(
isCollapsed
? FlowySvgs.hamburger_s_s
: FlowySvgs.pull_left_outlined_s,
),
),
);
}
}
class HiddenGroupList extends StatelessWidget {
const HiddenGroupList({
super.key,
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
final bloc = context.read<BoardBloc>();
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [
child,
MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
),
],
),
),
buildDefaultDragHandles: false,
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.only(bottom: 4),
key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"),
child: HiddenGroupCard(
group: state.hiddenGroups[index],
index: index,
bloc: bloc,
),
),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
}
}
class HiddenGroupCard extends StatefulWidget {
const HiddenGroupCard({
super.key,
required this.group,
required this.index,
required this.bloc,
});
final GroupPB group;
final BoardBloc bloc;
final int index;
@override
State<HiddenGroupCard> createState() => _HiddenGroupCardState();
}
class _HiddenGroupCardState extends State<HiddenGroupCard> {
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
final databaseController = widget.bloc.databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
return Padding(
padding: const EdgeInsets.only(left: 26),
child: AppFlowyPopover(
controller: _popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300),
popupBuilder: (popoverContext) => HiddenGroupPopupItemList(
bloc: widget.bloc,
viewId: databaseController.viewId,
groupId: widget.group.groupId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
),
child: HiddenGroupButtonContent(
popoverController: _popoverController,
groupId: widget.group.groupId,
index: widget.index,
bloc: widget.bloc,
),
),
);
}
}
class HiddenGroupButtonContent extends StatelessWidget {
final String groupId;
final int index;
final BoardBloc bloc;
const HiddenGroupButtonContent({
super.key,
required this.popoverController,
required this.groupId,
required this.index,
required this.bloc,
});
final PopoverController popoverController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: popoverController.show,
child: FlowyHover(
builder: (context, isHovering) {
return BlocProvider<BoardBloc>.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return SizedBox(
height: 30,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 3,
),
child: Row(
children: [
HiddenGroupCardActions(
isVisible: isHovering,
index: index,
),
const HSpace(4),
FlowyText.medium(
group.groupName,
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
Expanded(
child: FlowyText.medium(
group.rows.length.toString(),
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
),
if (isHovering) ...[
FlowyIconButton(
width: 20,
icon: FlowySvg(
FlowySvgs.show_m,
color: Theme.of(context).hintColor,
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
],
],
),
),
);
},
),
);
},
),
),
);
}
}
class HiddenGroupCardActions extends StatelessWidget {
final bool isVisible;
final int index;
const HiddenGroupCardActions({
super.key,
required this.isVisible,
required this.index,
});
@override
Widget build(BuildContext context) {
return ReorderableDragStartListener(
index: index,
enabled: isVisible,
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: SizedBox(
height: 14,
width: 14,
child: isVisible
? FlowySvg(
FlowySvgs.drag_element_s,
color: Theme.of(context).hintColor,
)
: const SizedBox.shrink(),
),
),
);
}
}
class HiddenGroupPopupItemList extends StatelessWidget {
const HiddenGroupPopupItemList({
required this.bloc,
required this.groupId,
required this.viewId,
required this.primaryField,
required this.rowCache,
super.key,
});
final BoardBloc bloc;
final String groupId;
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
);
});
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item)[primaryField.id]!,
primaryField: primaryField,
rowController: rowController,
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
renderHook: renderHook,
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
},
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
),
);
}
}
class HiddenGroupPopupItem extends StatelessWidget {
const HiddenGroupPopupItem({
super.key,
required this.cellContext,
required this.onPressed,
required this.cellBuilder,
required this.rowController,
required this.primaryField,
required this.renderHook,
});
final DatabaseCellContext cellContext;
final FieldInfo primaryField;
final RowController rowController;
final CardCellBuilder cellBuilder;
final RowCardRenderHook<String> renderHook;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: cellBuilder.buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
onTap: onPressed,
),
);
}
}
@@ -144,7 +144,7 @@ class EventPropertyList extends StatelessWidget {
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 11),
?.copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),
autofocus: true,
useRoundedBorder: true,
),
@@ -213,10 +213,13 @@ class _PropertyCellState extends State<PropertyCell> {
size: const Size.square(14),
),
const HSpace(4.0),
FlowyText.regular(
widget.cellContext.fieldInfo.name,
color: Theme.of(context).hintColor,
fontSize: 11,
Expanded(
child: FlowyText.regular(
widget.cellContext.fieldInfo.name,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
fontSize: 11,
),
),
],
),
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/startup/startup.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';
@@ -234,9 +233,9 @@ class LayoutDateField extends StatelessWidget {
offset: const Offset(-14, 0),
popupBuilder: (context) {
return BlocProvider(
create: (context) => getIt<DatabasePropertyBloc>(
param1: viewId,
param2: fieldController,
create: (context) => DatabasePropertyBloc(
viewId: viewId,
fieldController: fieldController,
)..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
builder: (context, state) {
@@ -6,13 +6,11 @@ import 'package:appflowy/plugins/database_view/application/row/row_service.dart'
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../application/database_controller.dart';
import 'dart:collection';
part 'grid_bloc.freezed.dart';
@@ -54,7 +52,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
didReceiveFieldUpdate: (fields) {
emit(
state.copyWith(
fields: GridFieldEquatable(fields),
fields: FieldList(fields),
),
);
},
@@ -175,7 +173,7 @@ class GridState with _$GridState {
const factory GridState({
required String viewId,
required Option<DatabasePB> grid,
required GridFieldEquatable fields,
required FieldList fields,
required List<RowInfo> rowInfos,
required int rowCount,
required LoadingState loadingState,
@@ -186,7 +184,7 @@ class GridState with _$GridState {
}) = _GridState;
factory GridState.initial(String viewId) => GridState(
fields: GridFieldEquatable(UnmodifiableListView([])),
fields: FieldList([]),
rowInfos: [],
rowCount: 0,
grid: none(),
@@ -199,26 +197,7 @@ class GridState with _$GridState {
);
}
class GridFieldEquatable extends Equatable {
final List<FieldInfo> _fieldInfos;
const GridFieldEquatable(
List<FieldInfo> fieldInfos,
) : _fieldInfos = fieldInfos;
@override
List<Object?> get props {
if (_fieldInfos.isEmpty) {
return [];
}
return [
_fieldInfos.length,
_fieldInfos
.map((fieldInfo) => fieldInfo.field.width)
.reduce((value, element) => value + element),
];
}
UnmodifiableListView<FieldInfo> get value =>
UnmodifiableListView(_fieldInfos);
@freezed
class FieldList with _$FieldList {
factory FieldList(List<FieldInfo> fields) = _FieldList;
}
@@ -21,7 +21,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
on<GridHeaderEvent>(
(event, emit) async {
await event.map(
initial: (_InitialHeader value) async {
initial: (_InitialHeader value) {
_startListening();
add(
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
@@ -65,7 +65,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
result.fold((l) {}, (err) => Log.error(err));
}
Future<void> _startListening() async {
void _startListening() {
fieldController.addListener(
onReceiveFields: (fields) =>
add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
@@ -132,7 +132,7 @@ class GridCellEquatable extends Equatable {
_fieldInfo.id,
_fieldInfo.fieldType,
_fieldInfo.field.visibility,
_fieldInfo.field.width,
_fieldInfo.fieldSettings?.width,
];
}
@@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -43,6 +44,14 @@ class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
),
);
},
updateIsEmpty: (isEmpty) async {
final unitOrFailure = await _rowBackendSvc.updateMeta(
rowId: rowId,
isDocumentEmpty: isEmpty,
);
unitOrFailure.fold((l) => null, (err) => Log.error(err));
},
);
},
);
@@ -104,6 +113,8 @@ class RowDocumentEvent with _$RowDocumentEvent {
_DidReceiveRowDocument;
const factory RowDocumentEvent.didReceiveError(FlowyError error) =
_DidReceiveError;
const factory RowDocumentEvent.updateIsEmpty(bool isDocumentEmpty) =
_UpdateIsEmpty;
}
@freezed
@@ -172,7 +172,7 @@ class _GridPageContentState extends State<GridPageContent> {
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final contentWidth = GridLayout.headerWidth(state.fields.value);
final contentWidth = GridLayout.headerWidth(state.fields.fields);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -6,7 +6,7 @@ class GridLayout {
if (fields.isEmpty) return 0;
final fieldsWidth = fields
.map((fieldInfo) => fieldInfo.field.width.toDouble())
.map((fieldInfo) => fieldInfo.fieldSettings!.width.toDouble())
.reduce((value, element) => value + element);
return fieldsWidth +
@@ -6,7 +6,7 @@ class GridSize {
static double get scrollBarSize => 8 * scale;
static double get headerHeight => 40 * scale;
static double get footerHeight => 40 * scale;
static double get leadingHeaderPadding => 50 * scale;
static double get leadingHeaderPadding => 40 * scale;
static double get trailHeaderPadding => 140 * scale;
static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale;
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -15,31 +15,41 @@ import 'field_cell_action_sheet.dart';
import 'field_type_extension.dart';
class GridFieldCell extends StatefulWidget {
final FieldContext cellContext;
final String viewId;
final FieldInfo fieldInfo;
const GridFieldCell({
Key? key,
required this.cellContext,
}) : super(key: key);
super.key,
required this.viewId,
required this.fieldInfo,
});
@override
State<GridFieldCell> createState() => _GridFieldCellState();
}
class _GridFieldCellState extends State<GridFieldCell> {
late final FieldCellBloc _bloc;
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
popoverController = PopoverController();
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
}
@override
didUpdateWidget(covariant oldWidget) {
if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) {
_bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo));
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return FieldCellBloc(fieldContext: widget.cellContext);
},
return BlocProvider.value(
value: _bloc,
child: BlocBuilder<FieldCellBloc, FieldCellState>(
builder: (context, state) {
final button = AppFlowyPopover(
@@ -50,11 +60,12 @@ class _GridFieldCellState extends State<GridFieldCell> {
controller: popoverController,
popupBuilder: (BuildContext context) {
return GridFieldCellActionSheet(
cellContext: widget.cellContext,
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
);
},
child: FieldCellButton(
field: widget.cellContext.fieldInfo.field,
field: widget.fieldInfo.field,
onTap: () => popoverController.show(),
),
);
@@ -78,6 +89,12 @@ class _GridFieldCellState extends State<GridFieldCell> {
),
);
}
@override
Future<void> dispose() async {
super.dispose();
await _bloc.close();
}
}
class _GridHeaderCellContainer extends StatelessWidget {
@@ -1,8 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -20,9 +20,13 @@ import '../../layout/sizes.dart';
import 'field_editor.dart';
class GridFieldCellActionSheet extends StatefulWidget {
final FieldContext cellContext;
const GridFieldCellActionSheet({required this.cellContext, Key? key})
: super(key: key);
final String viewId;
final FieldInfo fieldInfo;
const GridFieldCellActionSheet({
required this.viewId,
required this.fieldInfo,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _GridFieldCellActionSheetState();
@@ -37,31 +41,35 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
return SizedBox(
width: 400,
child: FieldEditor(
viewId: widget.cellContext.viewId,
fieldInfo: widget.cellContext.fieldInfo,
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.cellContext.viewId,
field: widget.cellContext.fieldInfo.field,
viewId: widget.viewId,
field: widget.fieldInfo.field,
),
),
);
}
return BlocProvider(
create: (context) =>
getIt<FieldActionSheetBloc>(param1: widget.cellContext),
create: (context) => FieldActionSheetBloc(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_EditFieldButton(
cellContext: widget.cellContext,
onTap: () {
setState(() => _showFieldEditor = true);
},
),
VSpace(GridSize.typeOptionSeparatorHeight),
_FieldOperationList(widget.cellContext),
_FieldOperationList(
viewId: widget.viewId,
fieldInfo: widget.fieldInfo,
),
],
),
),
@@ -70,10 +78,8 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
}
class _EditFieldButton extends StatelessWidget {
final FieldContext cellContext;
final void Function()? onTap;
const _EditFieldButton({required this.cellContext, Key? key, this.onTap})
: super(key: key);
const _EditFieldButton({Key? key, this.onTap}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -96,8 +102,13 @@ class _EditFieldButton extends StatelessWidget {
}
class _FieldOperationList extends StatelessWidget {
final FieldContext fieldContext;
const _FieldOperationList(this.fieldContext, {Key? key}) : super(key: key);
final String viewId;
final FieldInfo fieldInfo;
const _FieldOperationList({
required this.viewId,
required this.fieldInfo,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@@ -128,7 +139,7 @@ class _FieldOperationList extends StatelessWidget {
bool enable = true;
// If the field is primary, delete and duplicate are disabled.
if (fieldContext.fieldInfo.isPrimary) {
if (fieldInfo.isPrimary) {
switch (action) {
case FieldAction.hide:
break;
@@ -145,7 +156,8 @@ class _FieldOperationList extends StatelessWidget {
child: SizedBox(
height: GridSize.popoverItemHeight,
child: FieldActionCell(
fieldInfo: fieldContext,
viewId: viewId,
fieldInfo: fieldInfo,
action: action,
enable: enable,
),
@@ -155,11 +167,13 @@ class _FieldOperationList extends StatelessWidget {
}
class FieldActionCell extends StatelessWidget {
final FieldContext fieldInfo;
final String viewId;
final FieldInfo fieldInfo;
final FieldAction action;
final bool enable;
const FieldActionCell({
required this.viewId,
required this.fieldInfo,
required this.action,
required this.enable,
@@ -177,7 +191,7 @@ class FieldActionCell extends StatelessWidget {
? AFThemeExtension.of(context).textColor
: Theme.of(context).disabledColor,
),
onTap: () => action.run(context, fieldInfo),
onTap: () => action.run(context, viewId, fieldInfo),
leftIcon: FlowySvg(
action.icon(),
color: enable
@@ -217,7 +231,7 @@ extension _FieldActionExtension on FieldAction {
}
}
void run(BuildContext context, FieldContext fieldContext) {
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
switch (this) {
case FieldAction.hide:
context
@@ -228,8 +242,8 @@ extension _FieldActionExtension on FieldAction {
PopoverContainer.of(context).close();
FieldBackendService(
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.id,
viewId: viewId,
fieldId: fieldInfo.id,
).duplicateField();
break;
@@ -240,8 +254,8 @@ extension _FieldActionExtension on FieldAction {
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
FieldBackendService(
viewId: fieldContext.viewId,
fieldId: fieldContext.fieldInfo.field.id,
viewId: viewId,
fieldId: fieldInfo.field.id,
).deleteField();
},
).show(context);
@@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType {
FieldType.SingleSelect => true,
_ => false,
};
bool get canCreateNewGroup => switch (this) {
FieldType.MultiSelect => true,
FieldType.SingleSelect => true,
_ => false,
};
}
@@ -1,10 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -42,9 +40,9 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return getIt<GridHeaderBloc>(
param1: widget.viewId,
param2: widget.fieldController,
return GridHeaderBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
)..add(const GridHeaderEvent.initial());
},
child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
@@ -96,15 +94,10 @@ class _GridHeaderState extends State<_GridHeader> {
builder: (context, state) {
final cells = state.fields
.map(
(field) => FieldContext(
(fieldInfo) => GridFieldCell(
key: _getKeyById(fieldInfo.id),
viewId: widget.viewId,
fieldInfo: field,
),
)
.map(
(ctx) => GridFieldCell(
key: _getKeyById(ctx.fieldInfo.id),
cellContext: ctx,
fieldInfo: fieldInfo,
),
)
.toList();
@@ -136,7 +129,7 @@ class _GridHeaderState extends State<_GridHeader> {
int newIndex,
) {
if (cells.length > oldIndex) {
final field = cells[oldIndex].cellContext.fieldInfo.field;
final field = cells[oldIndex].fieldInfo.field;
context
.read<GridHeaderBloc>()
.add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
@@ -276,7 +276,7 @@ class RowContent extends StatelessWidget {
final GridCellWidget child = builder.build(cellId);
return CellContainer(
width: cellId.fieldInfo.field.width.toDouble(),
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
isPrimary: cellId.fieldInfo.field.isPrimary,
cellContainerNotifier: CellContainerNotifier(child),
accessoryBuilder: (buildContext) {
Loaded 100 of 752 files, more files were not shown because too many files have changed in this diff. Show more