Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e788c71602 | ||
|
|
7de2131431 | ||
|
|
47f3702ca9 | ||
|
|
3708a5b86a | ||
|
|
a63a7ea611 | ||
|
|
7867f0366e | ||
|
|
251c6d22b2 | ||
|
|
7cee8e392f | ||
|
|
765103dd22 | ||
|
|
50e612511d | ||
|
|
7eb20b232a | ||
|
|
3c7e636b65 | ||
|
|
889a313cc2 | ||
|
|
9586ea0e6f | ||
|
|
42e7317cd4 | ||
|
|
17651bf64c | ||
|
|
73f1c211c2 | ||
|
|
afc6473582 | ||
|
|
663f9d3423 | ||
|
|
5d4142d5b6 | ||
|
|
fe23183aef | ||
|
|
8116ea1dba | ||
|
|
3e6529aeb8 | ||
|
|
c4fc60612f | ||
|
|
4d82bb5322 | ||
|
|
2b684ae7bf | ||
|
|
eb54cf99d4 | ||
|
|
85af82acbe | ||
|
|
3434d447a7 | ||
|
|
1025b6d553 | ||
|
|
b35d6131d4 | ||
|
|
bc502c9c5b | ||
|
|
5f49c1748f | ||
|
|
73ea1a0685 | ||
|
|
a93d325e6a | ||
|
|
dc0af0f4c1 | ||
|
|
1ad85416d8 | ||
|
|
c34a7a92fb | ||
|
|
21d34d1fe0 | ||
|
|
3e088d48ac | ||
|
|
54dbcb7c5e |
No files matched your search
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-8
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
+136
@@ -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();
|
||||
}
|
||||
+53
-17
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+11
-7
@@ -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;
|
||||
}
|
||||
+16
-7
@@ -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
|
||||
|
||||
@@ -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>
|
||||
+3
-6
@@ -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
@@ -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';
|
||||
+16
-30
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-42
@@ -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,
|
||||
|
||||
+78
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+11
-6
@@ -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(),
|
||||
|
||||
+17
-8
@@ -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();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+48
-47
@@ -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
-1
@@ -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';
|
||||
|
||||
+30
-22
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+4
-7
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
-129
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+32
-18
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
+178
-135
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+104
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+208
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
+3
-3
@@ -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,
|
||||
),
|
||||
|
||||
+26
-24
@@ -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',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+102
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
@@ -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(
|
||||
|
||||
+62
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -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>(
|
||||
|
||||
+26
-48
@@ -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,
|
||||
);
|
||||
}
|
||||
-12
@@ -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;
|
||||
}
|
||||
+5
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+237
-118
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
-13
@@ -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),
|
||||
);
|
||||
|
||||
-112
@@ -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();
|
||||
}
|
||||
}
|
||||
+198
-115
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-236
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+120
-69
@@ -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(),
|
||||
};
|
||||
}
|
||||
+483
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+3
-4
@@ -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
-27
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -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)),
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ class GridCellEquatable extends Equatable {
|
||||
_fieldInfo.id,
|
||||
_fieldInfo.fieldType,
|
||||
_fieldInfo.field.visibility,
|
||||
_fieldInfo.field.width,
|
||||
_fieldInfo.fieldSettings?.width,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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 +
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+29
-12
@@ -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 {
|
||||
|
||||
+40
-26
@@ -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);
|
||||
|
||||
+6
@@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType {
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get canCreateNewGroup => switch (this) {
|
||||
FieldType.MultiSelect => true,
|
||||
FieldType.SingleSelect => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
+7
-14
@@ -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));
|
||||
|
||||
+1
-1
@@ -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
Reference in new issue
Block a user