diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml
index aa3dd6b90980..b64a51a00e31 100644
--- a/.github/workflows/flutter_ci.yaml
+++ b/.github/workflows/flutter_ci.yaml
@@ -23,7 +23,7 @@ on:
env:
CARGO_TERM_COLOR: always
- FLUTTER_VERSION: "3.10.4"
+ FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
CARGO_MAKE_VERSION: "0.36.6"
diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml
index 43c06aba9f83..17657eafadfd 100644
--- a/.github/workflows/mobile_ci.yaml
+++ b/.github/workflows/mobile_ci.yaml
@@ -18,7 +18,7 @@ on:
- "!frontend/appflowy_tauri/**"
env:
- FLUTTER_VERSION: "3.10.1"
+ FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
concurrency:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c8009aeb86d6..243062ac2b81 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,7 +6,7 @@ on:
- "*"
env:
- FLUTTER_VERSION: "3.10.1"
+ FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
jobs:
@@ -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: |
diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml
index 2121fcf6ae25..7d36e625e582 100644
--- a/.github/workflows/rust_coverage.yml
+++ b/.github/workflows/rust_coverage.yml
@@ -11,7 +11,7 @@ on:
env:
CARGO_TERM_COLOR: always
- FLUTTER_VERSION: "3.10.1"
+ FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
jobs:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b904f260ac8..3de85ea06062 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,43 @@
# 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
+- Support showing checklist items inline in row page.
+- Support inserting date from slash menu.
+- Support renaming a stack directly by clicking on the stack name.
+- Show the detailed reminder content in the notification center.
+- Save card order in Board view.
+- Allow to hide the ungrouped stack.
+- Segmented the checklist progress bar.
+
+### Bug fixes
+- Optimize side panel animation.
+- Fix calendar with hidden date or title doesn't show options correctly.
+- Fix the horizontal scroll bar disappears in Grid view.
+- Improve setting tab UI in Grid view.
+- Improve theme of the code block.
+- Fix some UI issues.
+
## Version 0.3.6 - 10/16/2023
### New Features
@@ -7,8 +45,7 @@
- Added Ukrainian language.
- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window.
- Support toggling the notifitcation on/off.
-- Added Lemonade theme.
-
+- Added Lemonade theme.
### Bug fixes
- Improve Vietnamese translations.
diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json
index 3c0301838479..a6027d9d1769 100644
--- a/frontend/.vscode/tasks.json
+++ b/frontend/.vscode/tasks.json
@@ -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"
- }
- }
]
}
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index 14e62b157d74..052ca05c4934 100644
--- a/frontend/Makefile.toml
+++ b/frontend/Makefile.toml
@@ -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
diff --git a/frontend/appflowy_flutter/README.md b/frontend/appflowy_flutter/README.md
index 8ae646031f69..116cd63f22b5 100644
--- a/frontend/appflowy_flutter/README.md
+++ b/frontend/appflowy_flutter/README.md
@@ -1,7 +1,7 @@
AppFlowy_Flutter
> Documentation for Contributors
diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt
new file mode 100644
index 000000000000..75b52484ea47
--- /dev/null
+++ b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt
@@ -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.
diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf
new file mode 100644
index 000000000000..61e5303325a1
Binary files /dev/null and b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf differ
diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf
new file mode 100644
index 000000000000..6df2b2536030
Binary files /dev/null and b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf differ
diff --git a/frontend/appflowy_flutter/dev.env b/frontend/appflowy_flutter/dev.env
index b00d233a2fbb..f51f1cbd9e20 100644
--- a/frontend/appflowy_flutter/dev.env
+++ b/frontend/appflowy_flutter/dev.env
@@ -1,23 +1,45 @@
# 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.
+# 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
+
-# 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:
+# 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
\ No newline at end of file
+# 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
+#
+# When using localhost for development, you must run AppFlowy Cloud locally
+# first. Plese Please follow the instructions below:
+# https://github.com/AppFlowy-IO/AppFlowy-Cloud#development
+#
+# After running AppFlowy Cloud locally, you can use the following settings:
+# 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=
diff --git a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart
new file mode 100644
index 000000000000..fe58e3836bb9
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart
@@ -0,0 +1,104 @@
+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';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import '../util/util.dart';
+
+const defaultFirstCardName = 'Card 1';
+const defaultLastCardName = 'Card 3';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('board add row test', () {
+ testWidgets('Add card from header', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapGoButton();
+
+ await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
+
+ final findFirstCard = find.descendant(
+ of: find.byType(AppFlowyGroupCard),
+ matching: find.byType(FlowyText),
+ );
+
+ FlowyText firstCardText = tester.firstWidget(findFirstCard);
+ expect(firstCardText.text, defaultFirstCardName);
+
+ await tester.tap(
+ find
+ .descendant(
+ of: find.byType(BoardColumnHeader),
+ matching: find.byWidgetPredicate(
+ (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
+ ),
+ )
+ .at(1),
+ );
+ await tester.pumpAndSettle();
+
+ const newCardName = 'Card 4';
+ await tester.enterText(
+ find.descendant(
+ of: find.byType(IntrinsicHeight),
+ matching: find.byType(TextField),
+ ),
+ newCardName,
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byType(AppFlowyBoard));
+ await tester.pumpAndSettle();
+
+ firstCardText = tester.firstWidget(findFirstCard);
+ expect(firstCardText.text, newCardName);
+ });
+
+ testWidgets('Add card from footer', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapGoButton();
+
+ await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
+
+ final findLastCard = find.descendant(
+ of: find.byType(AppFlowyGroupCard),
+ matching: find.byType(FlowyText),
+ );
+
+ FlowyText? lastCardText =
+ tester.widgetList(findLastCard).last as FlowyText;
+ expect(lastCardText.text, defaultLastCardName);
+
+ await tester.tap(
+ find
+ .descendant(
+ of: find.byType(AppFlowyGroupFooter),
+ matching: find.byType(FlowySvg),
+ )
+ .at(1),
+ );
+ await tester.pumpAndSettle();
+
+ const newCardName = 'Card 4';
+ await tester.enterText(
+ find.descendant(
+ of: find.byType(IntrinsicHeight),
+ matching: find.byType(TextField),
+ ),
+ newCardName,
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.byType(AppFlowyBoard));
+ await tester.pumpAndSettle();
+
+ lastCardText = tester.widgetList(findLastCard).last as FlowyText;
+ expect(lastCardText.text, newCardName);
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart
new file mode 100644
index 000000000000..46d91766d983
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart
@@ -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;
+ }
+}
diff --git a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart
index cdb5359f078d..46f9d969719b 100644
--- a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart
+++ b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart
@@ -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();
@@ -52,5 +55,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);
+ });
});
}
diff --git a/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart
index ebed667fb2b8..d95c1cbdd7b4 100644
--- a/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart
@@ -1,10 +1,12 @@
import 'package:integration_test/integration_test.dart';
import 'board_row_test.dart' as board_row_test;
+import 'board_add_row_test.dart' as board_add_row_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Board integration tests
board_row_test.main();
+ board_add_row_test.main();
}
diff --git a/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart b/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart
index b22993821103..909d13b49a45 100644
--- a/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart
+++ b/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
-import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart
index 62d5c81be31e..9a771eb876f9 100644
--- a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart
+++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart
@@ -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
diff --git a/frontend/appflowy_flutter/integration_test/database_share_test.dart b/frontend/appflowy_flutter/integration_test/database_share_test.dart
index bd42847f82c1..3582a6c74625 100644
--- a/frontend/appflowy_flutter/integration_test/database_share_test.dart
+++ b/frontend/appflowy_flutter/integration_test/database_share_test.dart
@@ -34,7 +34,7 @@ void main() {
false,
false,
false,
- false
+ false,
];
for (final (index, content) in checkboxCells.indexed) {
await tester.assertCheckboxCell(
@@ -54,7 +54,7 @@ void main() {
'10',
'11',
'12',
- ''
+ '',
];
for (final (index, content) in numberCells.indexed) {
await tester.assertCellContent(
@@ -152,7 +152,7 @@ void main() {
'Jun 16, 2023',
'',
'',
- ''
+ '',
];
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
diff --git a/frontend/appflowy_flutter/integration_test/database_view_test.dart b/frontend/appflowy_flutter/integration_test/database_view_test.dart
index 88c04eb45c52..740d1232ec5d 100644
--- a/frontend/appflowy_flutter/integration_test/database_view_test.dart
+++ b/frontend/appflowy_flutter/integration_test/database_view_test.dart
@@ -1,4 +1,4 @@
-import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
+import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
diff --git a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart
index c4c34841926e..902255055c87 100644
--- a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart
+++ b/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart
@@ -219,7 +219,7 @@ void main() {
expect(node.delta!.toJson(), [
{
'insert': text,
- 'attributes': {'href': url}
+ 'attributes': {'href': url},
}
]);
},
diff --git a/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart
new file mode 100644
index 000000000000..49d067f248a3
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart
@@ -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 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 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 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();
+}
diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart
index 2e9ca77f5ebb..42462c26585a 100644
--- a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart
@@ -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();
}
diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart
index 5bcf4b7b43a4..e92b849cd530 100644
--- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart
+++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart
@@ -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:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.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,
+ );
+ });
});
}
diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart
index e071f3ee363a..f33ce370ff7e 100644
--- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart
+++ b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart
@@ -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 insertingInlinePage(
+Future insertInlinePage(
WidgetTester tester,
ViewLayoutPB layout,
) async {
@@ -110,15 +110,19 @@ Future 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;
}
diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart
index 50b84ed521cc..80bdcbe98303 100644
--- a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart
+++ b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart
@@ -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(
diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart
index b120b5e33272..280ff2457733 100644
--- a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart
+++ b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart
@@ -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
diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart
index 84dac3900787..d34712920ee9 100644
--- a/frontend/appflowy_flutter/integration_test/runner.dart
+++ b/frontend/appflowy_flutter/integration_test/runner.dart
@@ -23,8 +23,11 @@ import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'tabs/tabs_test.dart' as tabs_test;
import 'empty_test.dart' as first_test;
import 'hotkeys_test.dart' as hotkeys_test;
-import 'settings/user_icon_test.dart' as user_icon_test;
-import 'settings/user_language_test.dart' as user_language_test;
+import 'import_files_test.dart' as import_files_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;
import 'panes/panes_test.dart' as panes_test;
import 'panes/panes_overlay_test.dart' as panes_overlay_test;
@@ -81,8 +84,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();
diff --git a/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart b/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart
new file mode 100644
index 000000000000..ab7458b2501f
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart
@@ -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);
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart
new file mode 100644
index 000000000000..2b5691569037
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart
@@ -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();
+}
diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart
index c0842b487b66..4aff4ca836b5 100644
--- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart
+++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart
@@ -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,
);
},
diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart
new file mode 100644
index 000000000000..e2681c2e9533
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart
@@ -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,
+ );
+ }
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart
index f4d54a2160c0..bf199036a83e 100644
--- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart
@@ -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();
}
diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart
index bfe6a8d833f0..b28188629bf1 100644
--- a/frontend/appflowy_flutter/integration_test/util/base.dart
+++ b/frontend/appflowy_flutter/integration_test/util/base.dart
@@ -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 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 pumpUntilFound(
diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart
index e8d160fec2c9..2e0775ada9ad 100644
--- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart
+++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart
@@ -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 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));
@@ -472,6 +483,47 @@ extension CommonOperations on WidgetTester {
}) async {
await tapButtonWithFlowySvgData(FlowySvgs.close_s, first);
}
+
+ // update the page icon in the sidebar
+ Future 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 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 {
diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart
index 89b94e6a2cbf..98ae23b2a2b6 100644
--- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart
+++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart
@@ -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';
@@ -35,10 +36,9 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/so
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
-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/tab_bar/tab_bar_header.dart';
+import 'package:appflowy/plugins/database_view/tab_bar/tab_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 openEmojiPicker() async {
await tapButton(find.byType(EmojiPickerButton));
- await tapButton(find.byType(EmojiSelectionMenu));
}
Future 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 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 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 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 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);
+ assert(
+ textFieldWidget.controller != null &&
+ textFieldWidget.controller!.text.isEmpty,
+ );
+ }
+
Future tapTabBarLinkedViewByViewName(String name) async {
final viewButton = findTabBarLinkViewByViewName(name);
await tapButton(viewButton);
diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart
index 5b3e8c07775a..73dd494922ae 100644
--- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart
+++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart
@@ -1,17 +1,20 @@
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:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.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 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 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 showAtMenu() async {
await tester.ime.insertCharacter('@');
}
diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/util/emoji.dart
index 616f3da6eccc..d439a9b3f7b6 100644
--- a/frontend/appflowy_flutter/integration_test/util/emoji.dart
+++ b/frontend/appflowy_flutter/integration_test/util/emoji.dart
@@ -1,17 +1,14 @@
-import 'package:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
extension EmojiTestExtension on WidgetTester {
- /// Must call [openEmojiPicker] first
- Future switchToEmojiList() async {
- final icon = find.byIcon(Icons.tag_faces);
- await tapButton(icon);
- }
-
Future tapEmoji(String emoji) async {
- final emojiWidget = find.text(emoji);
+ final emojiWidget = find.descendant(
+ of: find.byType(EmojiPicker),
+ matching: find.text(emoji),
+ );
await tapButton(emojiWidget);
}
}
diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart
index 56bd2c6804d7..b808ec7b841a 100644
--- a/frontend/appflowy_flutter/integration_test/util/expectation.dart
+++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart
@@ -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);
+ }
}
diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
index 66d63b248b21..2941bcb49b37 100644
--- a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
+++ b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart
@@ -15,7 +15,7 @@ class MyMockClient extends Mock implements http.Client {
if (requestType == 'POST' &&
requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = {
- 'content-type': 'text/event-stream'
+ 'content-type': 'text/event-stream',
};
final responseBody = Stream.fromIterable([
utf8.encode(
diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock
index 47e63e5fe9f3..b358068db882 100644
--- a/frontend/appflowy_flutter/ios/Podfile.lock
+++ b/frontend/appflowy_flutter/ios/Podfile.lock
@@ -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:
@@ -151,10 +171,13 @@ SPEC CHECKSUMS:
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
- file_picker: ce3938a0df3cc1ef404671531facef740d03f920
+ file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
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
diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
index cd5e7de15d61..50fd566bf0b0 100644
--- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -143,7 +143,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3db53b6e1fb7..b52b2e698b7e 100644
--- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
-
- NSPhotoLibraryUsageDescription
- This app requires access to the photo library.
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleLocalizations
-
- en
-
- CFBundleName
- AppFlowy
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLName
-
- CFBundleURLSchemes
-
- appflowy-flutter
-
-
-
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
-
-
+
+ NSCameraUsageDescription
+ AppFlowy requires access to the camera.
+ NSPhotoLibraryUsageDescription
+ AppFlowy requires access to the photo library.
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ en
+
+ FLTEnableImpeller
+
+ CFBundleName
+ AppFlowy
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+
+ CFBundleURLSchemes
+
+ appflowy-flutter
+
+
+
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart
similarity index 85%
rename from frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart
rename to frontend/appflowy_flutter/lib/env/backend_env.dart
index 63000278d6db..b9355e66ea86 100644
--- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart
+++ b/frontend/appflowy_flutter/lib/env/backend_env.dart
@@ -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 {
diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart
index 0c3dc86e2773..3cb2493926ea 100644
--- a/frontend/appflowy_flutter/lib/env/env.dart
+++ b/frontend/appflowy_flutter/lib/env/env.dart
@@ -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;
diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
index c32463c7d8b6..2a8a4ef75f59 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
@@ -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 lastPushedRouter =
+ PropertyValueNotifier('');
+}
+
extension MobileRouter on BuildContext {
Future pushView(ViewPB view) async {
+ await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
+ getIt().lastPushedRouter.value = view.routeName;
push(
Uri(
path: view.routeName,
diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart
index 55e827f10316..7d24ebc0a2fa 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart
@@ -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,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
index e63008f25fb6..77a975744fee 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
@@ -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 {
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 {
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 {
}
Widget _buildApp(ViewPB? view, List 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 {
context.read().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();
}
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart
index c1f43af889a0..a128f5434778 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart
@@ -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 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 createState() =>
- _MobileViewItemBottomSheetState();
-}
-
-class _MobileViewItemBottomSheetState extends State {
- 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().add(const ViewEvent.duplicate());
- break;
- case MobileViewItemBottomSheetBodyAction.share:
- // unimplemented
- context.pop();
- break;
- case MobileViewItemBottomSheetBodyAction.delete:
- context.pop();
- context.read().add(const ViewEvent.delete());
-
- break;
- case MobileViewItemBottomSheetBodyAction.addToFavorites:
- case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
- context.pop();
- context
- .read()
- .add(FavoriteEvent.toggle(widget.view));
- break;
- }
- },
- );
- case MobileBottomSheetType.rename:
- return MobileBottomSheetRenameWidget(
- name: widget.view.name,
- onRename: (name) {
- if (name != widget.view.name) {
- context.read().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';
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart
index 34c243d92ef0..b4aa9deee101 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart
@@ -1,8 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
-import 'package:appflowy/mobile/presentation/base/box_container.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
class BottomSheetActionWidget extends StatelessWidget {
const BottomSheetActionWidget({
@@ -10,42 +7,31 @@ class BottomSheetActionWidget extends StatelessWidget {
required this.svg,
required this.text,
required this.onTap,
+ this.iconColor,
});
final FlowySvgData svg;
final String text;
final VoidCallback onTap;
+ final Color? iconColor;
@override
Widget build(BuildContext context) {
- return FlowyBoxContainer(
- child: InkWell(
- onTap: () {
- HapticFeedback.mediumImpact();
- onTap();
- },
- enableFeedback: true,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 10.0,
- horizontal: 12.0,
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FlowySvg(
- svg,
- size: const Size.square(24.0),
- blendMode: BlendMode.dst,
- ),
- const HSpace(6.0),
- FlowyText(text),
- const Spacer(),
- ],
- ),
- ),
+ final iconColor =
+ this.iconColor ?? Theme.of(context).colorScheme.onBackground;
+
+ return OutlinedButton.icon(
+ icon: FlowySvg(
+ svg,
+ size: const Size.square(22.0),
+ color: iconColor,
),
+ label: Text(text),
+ style: Theme.of(context)
+ .outlinedButtonTheme
+ .style
+ ?.copyWith(alignment: Alignment.centerLeft),
+ onPressed: onTap,
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
index 4fec21177ed0..a89cf44f7b08 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
@@ -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,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart
new file mode 100644
index 000000000000..044e484fe9f4
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart
@@ -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 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,
+ ],
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart
index 9a8d6d2253a5..4e9fcd3d7e3e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart
@@ -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,
),
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart
index 6c4cf8924dcc..5a4806796c99 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart
@@ -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(),
@@ -93,7 +98,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
),
- )
+ ),
],
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart
index 1f186af7ae39..4643247b214a 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart
@@ -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,
+ ),
+ ),
+ IconButton(
+ icon: Icon(
+ Icons.close,
+ color: theme.hintColor,
+ ),
+ onPressed: () {
+ context.pop();
+ },
),
- // placeholder, ensure the title is centered
- const HSpace(40.0),
],
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart
index 69cbc1bc2c72..d1dd3527c32e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart
@@ -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 {
@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(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
index 34ec310da50f..fd41ca52bd07 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
@@ -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';
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
similarity index 84%
rename from frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart
rename to frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
index d6d252fe7c0f..c8116ff380e9 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
@@ -1,7 +1,4 @@
-import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart';
-import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart';
-import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart';
-import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
@@ -10,6 +7,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
+Future 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 {
@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 {
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
- context.read().add(const ViewEvent.duplicate());
context.pop();
+ context.read().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
- context.read().add(const ViewEvent.delete());
context.pop();
+ context.read().add(const ViewEvent.delete());
+
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
+ context.pop();
context
.read()
.add(FavoriteEvent.toggle(widget.view));
- context.pop();
break;
}
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart
index 84a0f20086dd..cef64bc19cb0 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart
@@ -91,7 +91,7 @@ class DetailsPlaceholderScreenState extends State {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
- ]
+ ],
],
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart
deleted file mode 100644
index 09ef2cf62709..000000000000
--- a/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart
+++ /dev/null
@@ -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,
- ),
- ),
- ],
- );
- }
-}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
index dd11796c82ac..07a36f2a47a5 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
@@ -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,12 +29,12 @@ class MobileFavoritePageFolder extends StatelessWidget {
BlocProvider(
create: (_) => MenuBloc(
user: userProfile,
- workspace: workspaceSetting.workspace,
+ workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
- )
+ ),
],
child: MultiBlocListener(
listeners: [
@@ -49,27 +49,27 @@ class MobileFavoritePageFolder extends StatelessWidget {
builder: (context) {
final favoriteState = context.watch().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),
+ ],
+ ),
),
),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
index 8f21404100b8..5541588d1008 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
@@ -19,7 +19,7 @@ class MobileFavoriteScreen extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
- FolderEventGetCurrentWorkspace().send(),
+ FolderEventGetCurrentWorkspaceSetting().send(),
getIt().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,
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
index 606206568278..f1d10ddc6788 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
@@ -29,12 +29,12 @@ class MobileFolders extends StatelessWidget {
BlocProvider(
create: (_) => MenuBloc(
user: user,
- workspace: workspaceSetting.workspace,
+ workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
- )
+ ),
],
child: MultiBlocListener(
listeners: [
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
index 0c5db1dfd8f3..ec54775d6217 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
@@ -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().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,
+ ),
),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
index 98777385b0e0..3cc4108b453a 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
@@ -66,7 +66,7 @@ class MobileHomePageHeader extends StatelessWidget {
icon: const Icon(
Icons.arrow_drop_down,
),
- )
+ ),
],
),
FlowyText.regular(
@@ -76,7 +76,7 @@ class MobileHomePageHeader extends StatelessWidget {
fontSize: 12,
color: theme.colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
- )
+ ),
],
),
),
@@ -87,7 +87,7 @@ class MobileHomePageHeader extends StatelessWidget {
icon: const FlowySvg(
FlowySvgs.m_setting_m,
),
- )
+ ),
],
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart
deleted file mode 100644
index 9fa880553d98..000000000000
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart
+++ /dev/null
@@ -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(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,
- ),
- ),
- ),
- ],
- ),
- );
- },
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
index 9f7289188547..0623b77430c3 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
@@ -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 {
return FutureBuilder(
future: getIt().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(),
- ],
- ),
- ),
- ),
+ ),
);
}),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
index 989107e2ffea..1870402b01c6 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
@@ -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()..add(const TrashEvent.initial()),
- child: Builder(
- builder: (context) {
+ child: BlocBuilder(
+ 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()
- .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();
+ 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()
- .add(const TrashEvent.deleteAll())
- ..pop();
- },
- ),
- )
- ],
+ );
+ },
),
- );
- },
- ),
],
),
- body: BlocBuilder(
- 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,104 +73,154 @@ class MobileHomeTrashPage extends StatelessWidget {
}
}
-class _DeletedFilesListView extends StatelessWidget {
- const _DeletedFilesListView(
- this.state,
- );
+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;
- final TrashState state;
@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,
- ),
- onPressed: () {
- context
- .read()
- .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().add(TrashEvent.delete(object));
- },
- )
- ],
- ),
- ),
- );
- },
- itemCount: state.objects.length,
+ 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 _TrashEmptyPage extends StatelessWidget {
- const _TrashEmptyPage();
+class _DeletedFilesListView extends StatelessWidget {
+ const _DeletedFilesListView(
+ this.state,
+ );
+ final TrashState state;
@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,
+ 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()
+ .add(TrashEvent.putback(object.id));
+ Fluttertoast.showToast(
+ msg:
+ '${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
+ gravity: ToastGravity.BOTTOM,
+ );
+ },
+ ),
+ IconButton(
+ splashRadius: 20,
+ icon: FlowySvg(
+ FlowySvgs.m_delete_m,
+ size: const Size.square(24),
+ color: theme.colorScheme.onSurface,
+ ),
+ onPressed: () {
+ context.read().add(TrashEvent.delete(object));
+ Fluttertoast.showToast(
+ msg:
+ '${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
+ gravity: ToastGravity.BOTTOM,
+ );
+ },
+ ),
+ ],
+ ),
),
- ),
- ],
+ );
+ },
+ itemCount: state.objects.length,
),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart
index 72a2897b31e7..c57d3b259222 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart
@@ -64,7 +64,7 @@ class MobilePersonalFolder extends StatelessWidget {
MobilePaneActionType.more,
]),
),
- )
+ ),
],
);
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart
new file mode 100644
index 000000000000..b4dbd0491037
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart
@@ -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 createState() => _MobileRecentFolderState();
+}
+
+class _MobileRecentFolderState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder(
+ valueListenable: getIt().lastPushedRouter,
+ builder: (context, value, child) {
+ return FutureBuilder>(
+ future: FolderEventReadRecentViews().send(),
+ builder: (context, snapshot) {
+ final recentViews = snapshot.data
+ ?.fold>(
+ (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 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,
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart
new file mode 100644
index 000000000000..de67e5b7dc1c
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart
@@ -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 createState() => _MobileRecentViewState();
+}
+
+class _MobileRecentViewState extends State {
+ 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(
+ 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 _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;
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
index d24806f24670..251332a7b231 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
@@ -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 {
_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(
@@ -315,7 +312,7 @@ class _SingleMobileInnerViewItemState extends State {
fontSize: 18.0,
overflow: TextOverflow.ellipsis,
),
- )
+ ),
];
// hover action
@@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State {
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 {
Widget _buildViewAddButton(BuildContext context) {
return MobileViewAddButton(
onPressed: () {
- showMobileBottomSheet(
- context: context,
- builder: (_) => AddNewPageWidgetBottomSheet(
- view: widget.view,
- onAction: (layout) {
- context.pop();
- context.read().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().add(
+ ViewEvent.createView(
+ LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ layout,
+ ),
+ );
+ },
+ );
+ },
);
},
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart
index d040f4cd27a1..d85e5fe2731f 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart
@@ -40,7 +40,7 @@ class _AppearanceSettingGroupState extends State {
color: theme.colorScheme.onSurface,
),
),
- const Icon(Icons.chevron_right)
+ const Icon(Icons.chevron_right),
],
),
onTap: () {
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart
index 4a68f411f575..026fb8466c62 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart
@@ -71,7 +71,7 @@ class _EditUsernameBottomSheetState extends State {
onPressed: () {
widget.context.pop();
},
- )
+ ),
],
),
const SizedBox(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart
index 1f177bed22ec..a2807732a156 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart
@@ -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,
),
@@ -62,7 +62,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
},
);
},
- )
+ ),
],
);
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart
index 5fcbf62fe8ac..3995e11b983c 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart
@@ -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',
- );
- },
- ),
- ],
+ ],
+ ),
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart
new file mode 100644
index 000000000000..383906955285
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart
@@ -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(),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart
index 58d13ce921a6..78086f413fc8 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart
@@ -8,6 +8,7 @@ Future showFlowyMobileBottomSheet(
}) async {
return showModalBottomSheet(
context: context,
+ isScrollControlled: true,
builder: (context) => Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
@@ -47,7 +48,7 @@ class _BottomSheetTitle extends StatelessWidget {
onPressed: () {
context.pop();
},
- )
+ ),
],
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart
new file mode 100644
index 000000000000..4966c1d9cd95
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart
@@ -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 showFlowyMobileConfirmDialog(
+ 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();
+ },
+ ),
+ ],
+ );
+ },
+ );
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart
new file mode 100644
index 000000000000..8c8b0ad0bd44
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart
@@ -0,0 +1,3 @@
+export 'show_flowy_mobile_confirm_dialog.dart';
+export 'show_flowy_mobile_bottom_sheet.dart';
+export 'flowy_mobile_state_container.dart';
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart
new file mode 100644
index 000000000000..4989bdb04321
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.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:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
+
+// use a global value to store the selected emoji to prevent reloading every time.
+EmojiData? _cachedEmojiData;
+
+class FlowyEmojiPicker extends StatefulWidget {
+ const FlowyEmojiPicker({
+ super.key,
+ required this.onEmojiSelected,
+ });
+
+ final EmojiSelectedCallback onEmojiSelected;
+
+ @override
+ State createState() => _FlowyEmojiPickerState();
+}
+
+class _FlowyEmojiPickerState extends State {
+ 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,
+ );
+ },
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart
new file mode 100644
index 000000000000..9619f00d30dc
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart
@@ -0,0 +1,48 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.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,
+ ),
+ ],
+ );
+ }
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart
new file mode 100644
index 000000000000..13ba942f4947
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart
@@ -0,0 +1,41 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_emoji_mart/flutter_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();
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart
new file mode 100644
index 000000000000..6aaa307ef5bb
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart
@@ -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(result);
+ },
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart
new file mode 100644
index 000000000000..34e897901af3
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart
@@ -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:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.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 createState() => _FlowyEmojiSearchBarState();
+}
+
+class _FlowyEmojiSearchBarState extends State {
+ 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('');
+ },
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart
new file mode 100644
index 000000000000..e8da112660b4
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart
@@ -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:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_emoji_mart/flutter_emoji_mart.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 createState() =>
+ _FlowyEmojiSkinToneSelectorState();
+}
+
+class _FlowyEmojiSkinToneSelectorState
+ extends State {
+ 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 '✋🏿';
+ }
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart
new file mode 100644
index 000000000000..818a3bcbf706
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart
@@ -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 createState() => _FlowyIconPickerState();
+}
+
+class _FlowyIconPickerState extends State
+ 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),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart
new file mode 100644
index 000000000000..292910b6f4ef
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart
@@ -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 createState() => _IconPickerPageState();
+}
+
+class _IconPickerPageState extends State {
+ @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,
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
index b37e990f21fa..40c644eec315 100644
--- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart
@@ -219,7 +219,7 @@ class CellController extends Equatable {
@override
List