Compare commits

...
54 Commits
Author SHA1 Message Date
Lucas.Xu 05603c9472 chore: bump version 0.5.7 2024-05-10 15:29:08 +08:00
Richard Shiue e28d463cb9 fix: kanban board card text input inconsistency (#5307) 2024-05-10 15:16:31 +08:00
Lucas.Xu bdc66103c8 fix: android navigation bar issues (#5294) 2024-05-10 15:00:48 +08:00
Lucas.Xu e2b6e14762 refactor: mobile view page (#5288)
* refactor: mobile view page

* fix: provider not found

* chore: add page style integration tests

* fix: android title bar padding

* fix: unable to click mentioned page on Android

* fix: font family not available log
2024-05-10 15:00:40 +08:00
Lucas.Xu 6220680ce0 feat: support system default font family on desktop (#5279)
* fix: add permission check before selecting image in image block

* feat: use system default font on desktop

* fix: set appbar icon size to 30

* feat: add default font family on desktop
2024-05-07 19:44:00 +08:00
Nathan.foooandLucas.Xu b4279f8004 fix: ws connect with invalid token (#5282)
* chore: bump client api

* chore: fix potentail ws connect with invalid token

* fix: cargo clippy

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-05-07 17:37:11 +08:00
Nathan.fooo dc813d85a8 chore: disable ai summary (#5277) 2024-05-07 12:38:44 +08:00
Lucas.Xu aa4fe2ba50 fix: color selection compatible with older versions (#5276)
* fix: color selection compatible with older versions

* chore: add a cover color function
2024-05-07 12:13:52 +08:00
Nathan.fooo a5b5a1f679 chore: bump client api (#5275) 2024-05-07 10:34:11 +08:00
Lucas.Xu 287c69a4fb fix: assert on uploading image on macos (#5272) 2024-05-07 09:11:35 +08:00
Mathias Mogensen c4f6703b76 fix: launch review improvements (#5263)
* fix: recent views fix

* fix: text button font color

* fix: sidebar scrollbar inset

* fix: account settings launch review

* fix: open + menu on mobile provider issue

* fix: code review

* fix: push view improvement
2024-05-06 18:13:17 +08:00
Nathan.fooo 3bdc8c222c chore: drop old user awarenss when open workspace (#5271) 2024-05-06 18:12:56 +08:00
Lucas.Xu 560529f7d9 fix: korean IME issue on Windows (#5267) 2024-05-06 16:23:13 +08:00
Lucas.Xu 9f44ae959a fix: the cursor will jump up when creating a new document on desktop. (#5269)
* fix: the cursor will jump up when creating a new document on desktop

* fix: the titlbar flashes when switching to another page

* fix: unit test

* fix: default font family copy
2024-05-06 15:50:32 +08:00
Lucas.Xu b2c366f6ae chore: remove debug print (#5268) 2024-05-06 13:34:59 +08:00
Nathan.fooo 3a9de61599 chore: update yrs version (#5262)
* chore: update yrs version

* chore: update client api
2024-05-06 11:30:13 +08:00
Lucas.Xu 266a2a53ab feat: request permission again if the user denied the photo permission (#5251)
* fix: clear the email field after sending email

* fix: ask permission before picking image

* feat: improve photo permission UI design

* chore: update translations

* fix: android photo permission

* chore: update translations

* fix: awareness meta data decode error
2024-05-06 11:27:55 +08:00
Richard Shiue d52042fa4f fix: open-row-as-full-page-related launch review issues (#5266) 2024-05-06 10:50:28 +08:00
Nathan.fooo a69e83c2cb feat: Implement summary field for database row (#5246)
* chore: impl summary field

* chore: draft ui

* chore: add summary event

* chore: impl desktop ui

* chore: impl mobile ui

* chore: update test

* chore: disable ai test
2024-05-05 22:04:34 +08:00
Richard Shiue 999ffeba21 chore: bump appflowy-board rev (#5260) 2024-05-05 11:16:09 +08:00
Lucas.Xu cb15018825 fix: google fonts not found exceptation (#5255) 2024-05-03 13:41:21 +08:00
Lucas.Xu 22e89fd197 fix: minor UI issues (#5253)
* fix: the desktop cover didn't sync with the mobile cover

* fix: remove unnecessary space when editing the title

* fix: recent view border issue

* fix: double focus on the title and doc
2024-05-03 09:13:11 +08:00
Lucas.Xu 554c821db9 fix: the desktop cover didn't sync with the mobile cover (#5252) 2024-05-03 09:12:49 +08:00
Richard Shiue 2eb88821ca fix: open row as full page launch review issues (#5254)
* perf: improve getting database speed

* fix: navigate to incorrect origin database view from full-width row

* fix: import row detail page action placement
2024-05-02 22:00:35 +08:00
Stefan Weiberg 813d024810 chore: update German translations and fixing English labels (#5233)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-05-02 14:10:36 +08:00
nathan 401ce7dfea chore: fix test 2024-05-02 13:46:15 +08:00
Lucas.Xu cadb177acd chore: upgrade editor version (#5244) 2024-05-02 12:10:46 +08:00
Lucas.Xu 016e131bd3 feat: adjust toolbar count based on the screen size (#5243) 2024-05-02 12:08:35 +08:00
Nathan.fooo 7831d8d4ab refactor: database row and cell notification (#5237)
* refactor: database row and cell notification

* chore: clippy

* chore: fix test
2024-05-02 11:42:33 +08:00
Mathias Mogensen e1e8747f15 feat: support mention page on mobile (#5158)
* feat: support mention page on mobile

* chore: clean up toggle notifier

* fix: changes after merge

* fix: depends on inherited widget error

* fix: amend after merge

* feat: add icon to search

* chore: slight style changes

* chore: revert podfile change

* ci: fix disposal
2024-05-02 02:10:56 +02:00
Lucas.Xu 6bfac6b80a feat: improve immersive cover style (#5241) 2024-05-01 22:24:55 +08:00
Chirag BargoojarandMathias Mogensen a971f3c6d2 fix: new group clear splash (#5214)
* fix: splash color for clear btn

* fix: translation for New group

* fix: complete rest of the issue

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-05-01 11:59:45 +02:00
Richard Shiue 1d73174b0c fix: export created at and last modified cells to csv (#5235)
* fix: export created at and last modified cells to csv

* fix: export csv test
2024-05-01 12:24:11 +08:00
Lucas.Xu 10a1571910 chore: revert auto expand feature (#5232) 2024-04-30 22:04:12 +08:00
Lucas.Xu 4d860cc404 feat: support inviting members (#5118)
* feat: support inviting members

* chore: update memeber error log

* feat: invite as member
2024-04-30 21:38:46 +08:00
Nathan.fooo 9a8109f5f8 chore: update client api and collab (#5231) 2024-04-30 20:40:03 +08:00
Mathias Mogensen 4981baac13 feat: settings my account (#5223)
* feat: my account settings page

* test: amend tests

* chore: remove unused code

* test: remove widget tests

* fix: text color on select buttons

* test: clean and remove unused test helpers

* test: fix test after refactor
2024-04-30 14:09:08 +02:00
Richard Shiue f3544375c9 fix: kanban UX bugs (#5227)
* chore: improve title editing behavior

* chore: fix editable text field

* chore: fix autoscroll
2024-04-30 17:35:03 +08:00
Lucas.Xu 33802fa62d feat: immersive page style on mobile (#5135) 2024-04-30 16:55:15 +08:00
Kilu.He 6d0598b101 feat: parse folder collab and display document title/icon/cover (#5222)
* feat: support web document and cypress test

* fix: support blocks

* fix: support table and outline

* fix: update nginx

* fix: support document title, icon, cover

fix: mock test folder
2024-04-30 11:32:26 +08:00
Richard Shiue e0d6b194bf fix: some database bugs (#5210)
* fix: open logic for url cell in database

* fix: row date created not working

* fix: help my toasts are burnt

* chore: quote style and add comment
2024-04-29 17:34:18 +08:00
Kilu.He 9135fb94ad feat: support web document and cypress test (#5116)
* feat: support web document and cypress test

* fix: support blocks

* fix: support table and outline

* fix: update nginx
2024-04-29 14:32:14 +08:00
Richard Shiue 969726ef73 feat: open a row as a full page (#5111)
* feat: open a row as a full page

* chore: don't set latest open view

* chore: fix calendar open

* chore: disable in relation

* chore: code cleanup

* chore: fix merge conflicts
2024-04-29 13:44:42 +08:00
Lucas.Xu 3c446d5e78 feat: use font name instead of name_regular (#5163) 2024-04-29 11:41:25 +08:00
Lucas.XuandMathias Mogensen 3fce5eebf8 fix: migrate OKRs issues (#5215)
* fix: disable color paraser when pasting texts

* fix: compile error

* fix: register hotkeys in init function

* fix: improve settings hotkey

* chore: update editor

* fix: view title overflow

* fix: integration test

* chore: improve auto expanding animation

* chore: bump version 0.5.6

* chore: enable collab workspace

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2024-04-29 11:41:09 +08:00
Richard Shiue 119fb03342 feat: mobile floating toolbar animation (#5170) 2024-04-29 10:33:57 +08:00
Richard Shiue 0b94e2b25d fix: kanban card icon not displaying (#5181) 2024-04-29 09:30:51 +08:00
Nathan.fooo 044dad1d3e chore: bump client api (#5217)
* chore: bump client api

* chore: fix compile

* chore: fix compile
2024-04-27 21:55:12 +08:00
Richard Shiue d4278a7549 chore: update documentation for type option cell-related traits (#5207) 2024-04-27 10:20:32 +08:00
Stefan Weiberg d467dcd68a chore: update translations with Fink 🐦 (#5200)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2024-04-26 20:11:46 +08:00
Korovandnathan 1ad351195d fix: websocket support custom port (#5123)
Co-authored-by: nathan <nathan@appflowy.io>
2024-04-26 20:09:55 +08:00
Nathan.fooo 122a392bf8 chore: update client api (#5208)
* chore: update client api

* chore: rename test target

* chore: fix test
2024-04-26 18:00:56 +08:00
Lucas.Xu d86afb03ba chore: add workspace log (#5206) 2024-04-26 14:09:02 +08:00
Nathan.fooo cc66147bc0 chore: checking workspace state consistent after switching workspace (#5201)
* refactor: getting workspace id

* refactor: check workspace id is match for http response

* refactor: check http repsonse in valid by checing the workspace id

* chore: update log

* chore: fix test

* chore: fix test

* chore: add test

* chore: update test
2024-04-26 09:44:07 +08:00
797 changed files with 20805 additions and 9831 deletions

No files matched your search

+15 -4
View File
@@ -8,6 +8,7 @@ on:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
pull_request:
branches:
@@ -16,6 +17,7 @@ on:
- ".github/workflows/mobile_ci.yaml"
- "frontend/**"
- "!frontend/appflowy_tauri/**"
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.19.0"
@@ -86,7 +88,16 @@ jobs:
model: 'iPhone 15'
shutdown_after_job: false
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
# - name: Run AppFlowy on simulator
# working-directory: frontend/appflowy_flutter
# run: |
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
# pid=$!
# sleep 500
# kill $pid
# continue-on-error: true
# enable it again if the 12 mins timeout is fixed
# - name: Run integration tests
# working-directory: frontend/appflowy_flutter
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
+1 -1
View File
@@ -96,7 +96,7 @@ jobs:
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
run: |
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart" -- --nocapture
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
+1 -1
View File
@@ -1,4 +1,4 @@
name: Tauri2-CI
name: Tauri-CI
on:
pull_request:
paths:
+3 -6
View File
@@ -1,11 +1,8 @@
name: Tauri-CI
on:
pull_request:
paths:
- ".github/workflows/tauri_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_tauri/**"
- "frontend/resources/**"
push:
branches:
- build/tauri
env:
NODE_VERSION: "18.16.0"
+4 -3
View File
@@ -1,4 +1,4 @@
name: Web2-CI
name: Web-CI
on:
pull_request:
paths:
@@ -17,14 +17,14 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest ]
platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -56,6 +56,7 @@ jobs:
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
pnpm run test:unit
- name: build and analyze
working-directory: frontend/appflowy_web_app
run: |
+48
View File
@@ -0,0 +1,48 @@
name: Cypress Tests
on:
pull_request:
paths:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
cypress-run:
if: github.event.pull_request.draft != true
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
# Install pnpm dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v6
with:
working-directory: frontend/appflowy_web_app
component: true
build: pnpm run build
start: pnpm run start
browser: chrome
+18
View File
@@ -1,4 +1,22 @@
# Release Notes
## Version 0.5.7 - 05/10/2024
### Bug Fixes
- Resolved page opening issue on Android.
- Fixed text input inconsistency on Kanban board cards.
## Version 0.5.6 - 05/07/2024
### New Features
- Team collaboration is live! Add members to your workspace to edit and collaborate on pages together.
- Collaborate in real time on the same page with other members. Edits made by others will appear instantly.
- Create multiple workspaces for different kinds of content.
- Customize your entire page on mobile through the Page Style menu with options for layout, font, font size, emoji, and cover image.
- Open a row record as a full page.
### Bug Fixes
- Resolved issue with setting background color for the Simple Table block.
- Adjusted toolbar for various screen sizes.
- Added a request for photo permission before uploading images on mobile.
- Exported creation and last modification timestamps to CSV.
## Version 0.5.5 - 04/24/2024
### New Features
- Improved the display of code blocks with line numbers
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.5.5"
APPFLOWY_VERSION = "0.5.7"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -47,6 +47,12 @@
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Permission to read files from external storage (outside application container).
As of Android 12 this permission no longer has any effect. Instead use the
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Permissions to read media files. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
@@ -11,6 +11,8 @@ const uint8_t *sync_event(const uint8_t *input, uintptr_t len);
int32_t set_stream_port(int64_t port);
int32_t set_log_stream_port(int64_t port);
void link_me_please(void);
void rust_log(int64_t level, const char *data);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

@@ -3,21 +3,28 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
@@ -37,22 +44,35 @@ void main() {
// reanme the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
await tester.enterText(userNameFinder, 'local_user');
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
await tester.pumpAndSettle();
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
// sign out
await tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.logout();
await tester.pumpAndSettle();
@@ -63,8 +83,9 @@ void main() {
// New anon user name
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userNameInput = tester.widget(userNameFinder) as UserNameInput;
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(userNameInput.name, 'Me');
});
});
@@ -6,8 +6,8 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -37,11 +37,18 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.tapButton(find.byType(SettingLogoutButton));
await tester.openSettingsPage(SettingsPage.account);
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeText(LocaleKeys.button_confirm.tr());
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
@@ -56,7 +63,16 @@ void main() {
// should not see the sync setting page when sign in as anonymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton();
});
@@ -8,14 +8,15 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
@@ -50,7 +51,7 @@ void main() {
await tester.waitForSeconds(6);
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
await tester.logout();
});
@@ -1,9 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -25,11 +23,8 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.tapButton(find.byType(SettingLogoutButton));
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
await tester.openSettingsPage(SettingsPage.account);
await tester.logout();
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
@@ -42,7 +37,16 @@ void main() {
// should not see the sync setting page when sign in as anonymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton();
});
@@ -2,22 +2,27 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/database_test_op.dart';
import '../shared/dir.dart';
import '../shared/emoji.dart';
@@ -39,28 +44,9 @@ void main() {
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
await tester.openSettingsPage(SettingsPage.account);
// Open icon picker dialog and select emoji
// await tester.tap(userAvatarFinder);
// await tester.pumpAndSettle();
// await tester.tapEmoji('😁');
// await tester.pumpAndSettle();
// final UserAvatar userAvatar =
// tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// enter user name
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
await tester.enterText(userNameFinder, name);
await tester.pumpAndSettle();
await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
@@ -78,23 +64,12 @@ void main() {
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// verify icon
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
// final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
// verify name
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
final UserNameInput userNameInput =
tester.widget(userNameFinder) as UserNameInput;
expect(userNameInput.name, name);
expect(profileSetting.name, name);
});
}
@@ -9,7 +9,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -2,6 +2,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
@@ -14,12 +16,10 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_worksp
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/widgets/card/container/card_container.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flutter/material.dart';
@@ -22,13 +22,15 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final findFirstCard = find.descendant(
of: find.byType(AppFlowyGroupCard),
matching: find.byType(Text),
);
final firstCard = find.byType(RowCard).first;
Text firstCardText = tester.firstWidget(findFirstCard);
expect(firstCardText.data, defaultFirstCardName);
expect(
find.descendant(
of: firstCard,
matching: find.text(defaultFirstCardName),
),
findsOneWidget,
);
await tester.tap(
find
@@ -45,7 +47,7 @@ void main() {
const newCardName = 'Card 4';
await tester.enterText(
find.descendant(
of: find.byType(RowCardContainer),
of: firstCard,
matching: find.byType(TextField),
),
newCardName,
@@ -55,8 +57,13 @@ void main() {
await tester.tap(find.byType(AppFlowyBoard));
await tester.pumpAndSettle();
firstCardText = tester.firstWidget(findFirstCard);
expect(firstCardText.data, newCardName);
expect(
find.descendant(
of: find.byType(RowCard).first,
matching: find.text(newCardName),
),
findsOneWidget,
);
});
testWidgets('from footer', (tester) async {
@@ -65,13 +72,15 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final findLastCard = find.descendant(
of: find.byType(AppFlowyGroupCard),
matching: find.byType(Text),
);
final lastCard = find.byType(RowCard).last;
Text? lastCardText = tester.widgetList(findLastCard).last as Text;
expect(lastCardText.data, defaultLastCardName);
expect(
find.descendant(
of: lastCard,
matching: find.text(defaultLastCardName),
),
findsOneWidget,
);
await tester.tap(
find
@@ -81,12 +90,11 @@ void main() {
)
.at(1),
);
await tester.pumpAndSettle();
const newCardName = 'Card 4';
await tester.enterText(
find.descendant(
of: find.byType(RowCardContainer),
of: lastCard,
matching: find.byType(TextField),
),
newCardName,
@@ -96,8 +104,13 @@ void main() {
await tester.tap(find.byType(AppFlowyBoard));
await tester.pumpAndSettle();
lastCardText = tester.widgetList(findLastCard).last as Text;
expect(lastCardText.data, newCardName);
expect(
find.descendant(
of: find.byType(RowCard).last,
matching: find.text(newCardName),
),
findsOneWidget,
);
});
});
}
@@ -1,10 +1,10 @@
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:appflowy_board/appflowy_board.dart';
import '../../shared/util.dart';
@@ -20,7 +20,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final card1 = find.ancestor(
of: find.text(card1Name),
matching: find.byType(AppFlowyGroupCard),
matching: find.byType(RowCard),
);
final doingGroup = find.text('Doing');
final doingGroupCenter = tester.getCenter(doingGroup);
@@ -106,58 +106,13 @@ void main() {
expect(node2.type, ParagraphBlockKeys.type);
expect(node3.type, ParagraphBlockKeys.type);
expect(node1.delta!.toJson(), [
{
"insert": "void",
"attributes": {"font_color": "0xfffede5d"},
},
{
"insert": " ",
"attributes": {"font_color": "0xffff7edb"},
},
{
"insert": "main",
"attributes": {"font_color": "0xff36f9f6"},
},
{
"insert": "() {",
"attributes": {"font_color": "0xffff7edb"},
}
{'insert': 'void main() {'},
]);
expect(node2.delta!.toJson(), [
{
"insert": " ",
"attributes": {"font_color": "0xffff7edb"},
},
{
"insert": "runApp",
"attributes": {"font_color": "0xff36f9f6"},
},
{
"insert": "(",
"attributes": {"font_color": "0xffff7edb"},
},
{
"insert": "const",
"attributes": {"font_color": "0xfffede5d"},
},
{
"insert": " ",
"attributes": {"font_color": "0xffff7edb"},
},
{
"insert": "MyApp",
"attributes": {"font_color": "0xfffe4450"},
},
{
"insert": "());",
"attributes": {"font_color": "0xffff7edb"},
}
{'insert': " runApp(const MyApp());"},
]);
expect(node3.delta!.toJson(), [
{
"insert": "}",
"attributes": {"font_color": "0xffff7edb"},
}
{"insert": "}"},
]);
});
});
@@ -1,3 +1,4 @@
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart';
@@ -73,7 +74,7 @@ void main() {
await tester.openSettingsPage(SettingsPage.files);
await tester.openSettingsPage(SettingsPage.appearance);
final resetButton = find.byKey(ThemeFontFamilySetting.resetButtonkey);
final resetButton = find.byKey(ThemeFontFamilySetting.resetButtonKey);
await tester.tap(resetButton);
await tester.pumpAndSettle();
@@ -82,8 +83,10 @@ void main() {
await tester.openSettingsPage(SettingsPage.appearance);
expect(
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
findsOneWidget,
find.textContaining(
DefaultAppearanceSettings.kDefaultFontFamily.fontFamilyDisplayName,
),
findsNWidgets(2),
);
});
});
@@ -14,7 +14,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
@@ -0,0 +1,139 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/dir.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document page style', () {
double getCurrentEditorFontSize() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.fontSize!;
}
double getCurrentEditorLineHeight() {
final editorPage = find
.byType(AppFlowyEditorPage)
.evaluate()
.single
.widget as AppFlowyEditorPage;
return editorPage.styleCustomizer
.style()
.textStyleConfiguration
.text
.height!;
}
testWidgets('change font size in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize);
// change font size from normal to large
await tester.tapSvgButton(FlowySvgs.m_font_size_large_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize);
// change font size from large to small
await tester.tapSvgButton(FlowySvgs.m_font_size_small_s);
expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize);
});
testWidgets('change line height in page style settings', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.normal.lineHeight,
);
// change line height from normal to large
await tester.tapSvgButton(FlowySvgs.m_layout_large_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.large.lineHeight,
);
// change line height from large to small
await tester.tapSvgButton(FlowySvgs.m_layout_small_s);
expect(
getCurrentEditorLineHeight(),
PageStyleLineHeightLayout.small.lineHeight,
);
});
testWidgets('use built-in image as cover', (tester) async {
await tester.launchInAnonymousMode();
// click the getting start page
await tester.openPage(gettingStarted);
// click the layout button
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
// toggle the preset button
await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m);
// select the first preset
final firstBuiltInImage = find.byWidgetPredicate(
(widget) =>
widget is Image &&
widget.image is AssetImage &&
(widget.image as AssetImage).assetName ==
PageStyleCoverImageType.builtInImagePath('1'),
);
await tester.tap(firstBuiltInImage);
// click done button to exit the page style settings
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
await tester.tapButton(find.byType(BottomSheetDoneButton).first);
// check the cover
final builtInCover = find.descendant(
of: find.byType(DocumentImmersiveCover),
matching: firstBuiltInImage,
);
expect(builtInCover, findsOneWidget);
});
});
}
@@ -11,7 +11,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
@@ -1,8 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
}
@@ -1,25 +1,33 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
import 'expectation.dart';
import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
}
/// Requires being on the SettingsPage.account of the SettingsDialog
Future<void> logout() async {
await tapButton(find.byType(SettingLogoutButton));
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: scrollable,
);
expectToSeeText(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr());
await tapButton(find.byType(SignInOutButton));
expectToSeeText(LocaleKeys.button_confirm.tr());
await tapButtonWithName(LocaleKeys.button_confirm.tr());
}
Future<void> tapSignInAsGuest() async {
@@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
@@ -13,16 +16,12 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
class FlowyTestContext {
FlowyTestContext({
required this.applicationDataDirectory,
});
FlowyTestContext({required this.applicationDataDirectory});
final String applicationDataDirectory;
}
@@ -75,7 +74,7 @@ extension AppFlowyTestBase on WidgetTester {
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
await useLocal();
await useLocalServer();
break;
case AuthenticatorType.supabase:
await useTestSupabaseCloud();
@@ -187,37 +186,14 @@ extension AppFlowyTestBase on WidgetTester {
);
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
}) async {
Finder button = find.text(
tr,
findRichText: true,
skipOffstage: false,
);
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(
button,
milliseconds: milliseconds,
);
return;
}
Future<void> tapButtonWithTooltip(
String tr, {
int milliseconds = 500,
}) async {
final button = find.byTooltip(tr);
await tapButton(
button,
milliseconds: milliseconds,
);
return;
await tapButton(button, milliseconds: milliseconds);
}
Future<void> doubleTapAt(
@@ -232,34 +208,8 @@ extension AppFlowyTestBase on WidgetTester {
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> doubleTapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tap(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pump(kDoubleTapMinTime);
await tap(
finder,
buttons: buttons,
pointer: pointer,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
}
@@ -271,10 +221,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
}
}
Future<void> useLocal() async {
await useLocalServer();
}
Future<void> useTestSupabaseCloud() async {
await useSupabaseCloud(
url: TestEnv.supabaseUrl,
@@ -1,13 +1,10 @@
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
@@ -24,12 +21,17 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_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-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'emoji.dart';
@@ -72,27 +74,6 @@ extension CommonOperations on WidgetTester {
await tapButton(newPageButton);
}
/// Tap the create document button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateDocumentButton() async {
await tapButtonWithName(LocaleKeys.document_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateGridButton() async {
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateCalendarButton() async {
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
}
/// Tap the import button.
///
/// Must call [tapAddViewButton] first.
@@ -181,15 +162,9 @@ extension CommonOperations on WidgetTester {
}) async {
final pageNames = findPageName(name, layout: layout);
if (useLast) {
await hoverOnWidget(
pageNames.last,
onHover: onHover,
);
await hoverOnWidget(pageNames.last, onHover: onHover);
} else {
await hoverOnWidget(
pageNames.first,
onHover: onHover,
);
await hoverOnWidget(pageNames.first, onHover: onHover);
}
}
@@ -497,9 +472,7 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> openNotificationHub({
int tabIndex = 0,
}) async {
Future<void> openNotificationHub({int tabIndex = 0}) async {
final finder = find.descendant(
of: find.byType(NotificationButton),
matching: find.byWidgetPredicate(
@@ -542,15 +515,6 @@ extension CommonOperations on WidgetTester {
await tapButton(workspace, milliseconds: 2000);
}
Future<void> closeCollaborativeWorkspaceMenu() async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
}
await tapAt(Offset.zero);
await pumpAndSettle();
}
Future<void> createCollaborativeWorkspace(String name) async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
@@ -574,6 +538,44 @@ extension CommonOperations on WidgetTester {
await tapButtonWithName(LocaleKeys.button_ok.tr());
}
// For mobile platform to launch the app in anonymous mode
Future<void> launchInAnonymousMode() async {
assert(
[TargetPlatform.android, TargetPlatform.iOS]
.contains(defaultTargetPlatform),
'This method is only supported on mobile platforms',
);
await initializeAppFlowy();
final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);
expect(anonymousSignInButton, findsOneWidget);
await tapButton(anonymousSignInButton);
await pumpUntilFound(find.byType(MobileHomeScreen));
}
Future<void> tapSvgButton(FlowySvgData svg) async {
final button = find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
);
await tapButton(button);
}
}
extension SettingsFinder on CommonFinders {
Finder findSettingsScrollable() => find
.descendant(
of: find
.descendant(
of: find.byType(SettingsBody),
matching: find.byType(SingleChildScrollView),
)
.first,
matching: find.byType(Scrollable),
)
.first;
}
extension ViewLayoutPBTest on ViewLayoutPB {
@@ -1,8 +1,9 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -51,11 +52,7 @@ class TestWorkspaceService {
Future<void> setUpAll() async {
final root = await workspace.root;
final path = root.path;
SharedPreferences.setMockInitialValues(
{
KVKeys.pathLocation: path,
},
);
SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path});
}
/// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.
@@ -1,7 +1,5 @@
import 'dart:io';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -31,8 +29,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@@ -57,6 +53,10 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
@@ -71,7 +71,6 @@ import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@@ -343,16 +342,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
return w.isToday;
}
Future<void> toggleDateRange() async {
final findDateEditor = find.byType(EndTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
matching: findToggle,
);
await tapButton(finder);
}
Future<void> tapChangeDateTimeFormatButton() async {
await tapButton(find.byType(DateTypeOptionButton));
}
@@ -403,9 +392,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// The [SelectOptionCellEditor] must be opened first.
Future<void> createOption({
required String name,
}) async {
Future<void> createOption({required String name}) async {
final findEditor = find.byType(SelectOptionCellEditor);
expect(findEditor, findsOneWidget);
@@ -419,9 +406,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> selectOption({
required String name,
}) async {
Future<void> selectOption({required String name}) async {
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
);
@@ -440,11 +425,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget.name == name || widget.option?.name == name),
);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: option,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: option);
expect(cell, findsOneWidget);
}
@@ -458,11 +439,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is SelectOptionTag,
);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: options,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: options);
expect(cell, matcher);
}
@@ -470,21 +447,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
final findRow = find.byType(GridRow);
final findCell = finderForFieldType(FieldType.Checklist);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: findCell,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell);
await tapButton(cell);
}
void assertChecklistEditorVisible({required bool visible}) {
final editor = find.byType(ChecklistCellEditor);
if (visible) {
expect(editor, findsOneWidget);
} else {
expect(editor, findsNothing);
return expect(editor, findsOneWidget);
}
expect(editor, findsNothing);
}
Future<void> createNewChecklistTask({
@@ -519,7 +491,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
required bool isChecked,
}) {
final task = find.byType(ChecklistItem).at(index);
final widget = this.widget<ChecklistItem>(task);
assert(
widget.task.data.name == name && widget.task.isSelected == isChecked,
@@ -591,27 +562,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
Future<void> editTitleInRowDetailPage(String title) async {
final titleField = find.byType(EditableTextCell);
await enterText(titleField, title);
await pumpAndSettle();
}
Future<void> hoverRowBanner() async {
final banner = find.byType(RowBanner);
expect(banner, findsOneWidget);
await startGesture(
getCenter(banner),
kind: PointerDeviceKind.mouse,
);
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async {
await tapButton(find.byType(AddEmojiButton));
}
Future<void> openEmojiPicker() async =>
tapButton(find.byType(AddEmojiButton));
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(EditableDateCell);
@@ -630,25 +590,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> duplicateRowInRowDetailPage() async {
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
await tapButton(duplicateButton);
}
Future<void> deleteRowInRowDetailPage() async {
final deleteButton = find.byType(RowDetailPageDeleteButton);
await tapButton(deleteButton);
}
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
final fieldButtons = find.byType(FieldCellButton);
final button = find
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
.at(index);
return startGesture(
getCenter(button),
kind: PointerDeviceKind.mouse,
);
return startGesture(getCenter(button), kind: PointerDeviceKind.mouse);
}
Future<void> reorderFieldInRowDetail({required double offset}) async {
@@ -657,11 +604,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is ReorderableDragStartListener && widget.enabled,
)
.first;
await drag(
thumb,
Offset(0, offset),
kind: PointerDeviceKind.mouse,
);
await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
}
@@ -681,8 +624,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
(w) => w is FieldActionCell && w.action == FieldAction.delete,
);
await tapButton(deleteButton);
@@ -693,11 +635,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(confirmButton);
}
Future<void> scrollGridByOffset(Offset offset) async {
await drag(find.byType(GridPage), offset);
await pumpAndSettle();
}
Future<void> scrollRowDetailByOffset(Offset offset) async {
await drag(find.byType(RowDetailPage), offset);
await pumpAndSettle();
@@ -756,8 +693,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapDeletePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
(w) => w is FieldActionCell && w.action == FieldAction.delete,
);
await tapButton(field);
}
@@ -765,9 +701,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// A SimpleDialog must be shown first, e.g. when deleting a field.
Future<void> tapDialogOkButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is PrimaryTextButton &&
widget.label == LocaleKeys.button_ok.tr(),
(w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(),
);
await tapButton(field);
}
@@ -775,8 +709,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapDuplicatePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.duplicate,
(w) => w is FieldActionCell && w.action == FieldAction.duplicate,
);
await tapButton(field);
}
@@ -798,45 +731,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
);
await tapButton(field);
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
);
await tapButton(button);
}
Future<void> tapRowDetailPageRowActionButton() async {
await tapButton(find.byType(RowActionButton));
}
Future<void> tapRowDetailPageRowActionButton() async =>
tapButton(find.byType(RowActionButton));
Future<void> tapRowDetailPageCreatePropertyButton() async {
await tapButton(find.byType(CreateRowFieldButton));
}
Future<void> tapRowDetailPageCreatePropertyButton() async =>
tapButton(find.byType(CreateRowFieldButton));
Future<void> tapRowDetailPageDeleteRowButton() async {
await tapButton(find.byType(RowDetailPageDeleteButton));
}
Future<void> tapRowDetailPageDeleteRowButton() async =>
tapButton(find.byType(RowDetailPageDeleteButton));
Future<void> tapRowDetailPageDuplicateRowButton() async {
await tapButton(find.byType(RowDetailPageDuplicateButton));
}
Future<void> tapRowDetailPageDuplicateRowButton() async =>
tapButton(find.byType(RowDetailPageDuplicateButton));
Future<void> tapSwitchFieldTypeButton() async {
await tapButton(find.byType(SwitchFieldButton));
}
Future<void> tapSwitchFieldTypeButton() async =>
tapButton(find.byType(SwitchFieldButton));
Future<void> tapEscButton() async {
await sendKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape);
/// Must call [tapSwitchFieldTypeButton] first.
Future<void> selectFieldType(FieldType fieldType) async {
@@ -851,15 +773,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
// Use in edit mode of FieldEditor
void expectEmptyTypeOptionEditor() {
expect(
find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.byType(TypeOptionSeparator),
),
findsNothing,
);
}
void expectEmptyTypeOptionEditor() => expect(
find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.byType(TypeOptionSeparator),
),
findsNothing,
);
/// Each field has its own cell, so we can find the corresponding cell by
/// the field type after create a new field.
@@ -868,10 +788,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, findsWidgets);
}
Future<void> assertNumberOfFieldsInGridPage(int num) async {
expect(find.byType(GridFieldCell), findsNWidgets(num));
}
Future<void> assertNumberOfRowsInGridPage(int num) async {
expect(
find.byType(GridRow, skipOffstage: false),
@@ -884,14 +800,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Check the field type of the [FieldCellButton] is the same as the name.
Future<void> assertFieldTypeWithFieldName(
String name,
FieldType fieldType,
) async {
Future<void> assertFieldTypeWithFieldName(String name, FieldType type) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldCellButton &&
widget.field.fieldType == fieldType &&
widget.field.fieldType == type &&
widget.field.name == name,
);
@@ -936,11 +849,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> findFieldEditor(dynamic matcher) async {
final finder = find.byType(FieldEditor);
expect(finder, matcher);
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
@@ -994,41 +902,29 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(SortButton));
}
Future<void> tapCreateFilterByFieldType(
FieldType fieldType,
String title,
) async {
Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {
final findFilter = find.byWidgetPredicate(
(widget) =>
widget is GridFilterPropertyCell &&
widget.fieldInfo.fieldType == fieldType &&
widget.fieldInfo.fieldType == type &&
widget.fieldInfo.name == title,
);
await tapButton(findFilter);
}
Future<void> tapFilterButtonInGrid(String filterName) async {
Future<void> tapFilterButtonInGrid(String name) async {
final findFilter = find.byType(FilterMenuItem);
final button = find.descendant(
of: findFilter,
matching: find.text(filterName),
);
final button = find.descendant(of: findFilter, matching: find.text(name));
await tapButton(button);
}
Future<void> tapCreateSortByFieldType(
FieldType fieldType,
String title,
) async {
Future<void> tapCreateSortByFieldType(FieldType type, String title) async {
final findSort = find.byWidgetPredicate(
(widget) =>
widget is GridSortPropertyCell &&
widget.fieldInfo.fieldType == fieldType &&
widget.fieldInfo.fieldType == type &&
widget.fieldInfo.name == title,
);
await tapButton(findSort);
}
@@ -1085,10 +981,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
of: fromSortItem,
matching: find.byType(ReorderableDragStartListener),
);
await drag(
dragElement,
getCenter(toSortItem) - getCenter(fromSortItem),
);
await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem));
await pumpAndSettle(const Duration(milliseconds: 200));
}
@@ -1166,15 +1059,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findCell);
}
Future<void> tapCheckedButtonOnCheckboxFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
);
await tapButton(button);
}
Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
@@ -1193,15 +1077,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapUnCompletedButtonOnChecklistFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
);
await tapButton(button);
}
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingsList);
@@ -1252,16 +1127,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapFirstDayOfWeek() async {
await tapButton(find.byType(FirstDayOfWeek));
}
Future<void> tapFirstDayOfWeekStartFromSunday() async {
final finder = find.byWidgetPredicate(
(widget) => widget is StartFromButton && widget.dayIndex == 0,
);
await tapButton(finder);
}
Future<void> tapFirstDayOfWeek() async =>
tapButton(find.byType(FirstDayOfWeek));
Future<void> tapFirstDayOfWeekStartFromMonday() async {
final finder = find.byWidgetPredicate(
@@ -1277,20 +1144,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
void assertFirstDayOfWeekStartFromMonday() {
final finder = find.byWidgetPredicate(
(widget) =>
widget is StartFromButton &&
widget.dayIndex == 1 &&
widget.isSelected == true,
(w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true,
);
expect(finder, findsOneWidget);
}
void assertFirstDayOfWeekStartFromSunday() {
final finder = find.byWidgetPredicate(
(widget) =>
widget is StartFromButton &&
widget.dayIndex == 0 &&
widget.isSelected == true,
(w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true,
);
expect(finder, findsOneWidget);
}
@@ -1307,11 +1168,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
),
)
.first;
await scrollUntilVisible(
todayCell,
300,
scrollable: scrollable,
);
await scrollUntilVisible(todayCell, 300, scrollable: scrollable);
await pumpAndSettle(const Duration(milliseconds: 300));
}
@@ -1351,12 +1208,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
String? title,
}) {
final findDayCell = find.byWidgetPredicate(
(widget) =>
widget is CalendarDayCard &&
isSameDay(
widget.date,
date,
),
(widget) => widget is CalendarDayCard && isSameDay(widget.date, date),
);
Finder findEvents = find.descendant(
of: findDayCell,
@@ -1390,13 +1242,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cards.at(index));
}
void assertEventEditorOpen() {
expect(find.byType(CalendarEventEditor), findsOneWidget);
}
void assertEventEditorOpen() =>
expect(find.byType(CalendarEventEditor), findsOneWidget);
Future<void> dismissEventEditor() async {
await simulateKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> dismissEventEditor() async =>
simulateKeyEvent(LogicalKeyboardKey.escape);
Future<void> editEventTitle(String title) async {
final textField = find.descendant(
@@ -1507,10 +1357,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: find.byType(TextField),
);
if (isVisible) {
expect(textField, findsOneWidget);
} else {
expect(textField, findsNothing);
return expect(textField, findsOneWidget);
}
expect(textField, findsNothing);
}
Future<void> enterNewGroupName(String name, {required bool submit}) async {
@@ -1612,21 +1461,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(okButton);
}
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
expect(find.byType(BoardPage), findsOneWidget);
break;
case DatabaseLayoutPB.Calendar:
expect(find.byType(CalendarPage), findsOneWidget);
break;
case DatabaseLayoutPB.Grid:
expect(find.byType(GridPage), findsOneWidget);
break;
default:
throw Exception('Unknown database layout type: $layout');
}
}
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board =>
expect(find.byType(BoardPage), findsOneWidget),
DatabaseLayoutPB.Calendar =>
expect(find.byType(CalendarPage), findsOneWidget),
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
_ => throw Exception('Unknown database layout type: $layout'),
};
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
@@ -1634,11 +1476,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is FlowyText && widget.text == layout.layoutName,
);
final button = find.descendant(
of: findLayoutCell,
matching: findText,
);
final button = find.descendant(of: findLayoutCell, matching: findText);
await tapButton(button);
}
@@ -1660,8 +1498,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(
find.byWidgetPredicate(
(widget) =>
widget is NumberFormatCell && widget.format == NumberFormatPB.USD,
(w) => w is NumberFormatCell && w.format == NumberFormatPB.USD,
),
);
}
@@ -1675,8 +1512,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
String fieldName,
) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
(w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName,
);
final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton));
@@ -1684,18 +1520,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return find.byType(BoardPage);
case DatabaseLayoutPB.Calendar:
return find.byType(CalendarPage);
case DatabaseLayoutPB.Grid:
return find.byType(GridPage);
default:
throw Exception('Unknown database layout type: $layout');
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board => find.byType(BoardPage),
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
DatabaseLayoutPB.Grid => find.byType(GridPage),
_ => throw Exception('Unknown database layout type: $layout'),
};
Finder finderForFieldType(FieldType fieldType) {
switch (fieldType) {
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:archive/archive.dart';
import 'package:path/path.dart' as p;
Future<void> deleteDirectoriesWithSameBaseNameAsPrefix(
String path,
@@ -1,9 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
@@ -17,6 +14,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embe
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';
@@ -31,11 +30,8 @@ class EditorOperations {
final WidgetTester tester;
EditorState getCurrentEditorState() {
return tester
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
.editorState;
}
EditorState getCurrentEditorState() =>
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
@@ -66,7 +62,7 @@ class EditorOperations {
Future<void> tapGettingStartedIcon() async {
await tester.tapButton(
find.descendant(
of: find.byType(DocumentHeaderNodeWidget),
of: find.byType(DocumentCoverWidget),
matching: find.findTextInFlowyText('⭐️'),
),
);
@@ -145,16 +141,8 @@ class EditorOperations {
);
}
Future<void> switchNetworkImageCover(String imageUrl) async {
final image = find.byWidgetPredicate(
(widget) => widget is ImageGridItem,
);
await tester.tapButton(image);
}
Future<void> tapOnRemoveCover() async {
await tester.tapButton(find.byType(DeleteCoverButton));
}
Future<void> tapOnRemoveCover() async =>
tester.tapButton(find.byType(DeleteCoverButton));
/// A cover must be present in the document to function properly since this
/// catches all cover types collectively
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
@@ -10,6 +11,7 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -89,18 +91,6 @@ extension Expectation on WidgetTester {
expect(exportSuccess, findsOneWidget);
}
/// Expect to see the add button and icon button in the cover toolbar
void expectToSeePluginAddCoverAndIconButton() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsOneWidget);
expect(addIcon, findsOneWidget);
}
/// Expect to see the document header toolbar empty
void expectToSeeEmptyDocumentHeaderToolbar() {
final addCover = find.textContaining(
@@ -153,14 +143,6 @@ extension Expectation on WidgetTester {
expect(findRemoveIcon, findsOneWidget);
}
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == name,
);
expect(userName, findsOneWidget);
}
/// Expect to see a text
void expectToSeeText(String text) {
Finder textWidget = find.textContaining(text, findRichText: true);
@@ -178,26 +160,23 @@ extension Expectation on WidgetTester {
ViewLayoutPB layout = ViewLayoutPB.Document,
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
}) =>
find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
Finder findAllFavoritePages() {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
);
}
Finder findAllFavoritePages() => find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
);
Finder findPageName(
String name, {
@@ -205,25 +184,35 @@ extension Expectation on WidgetTester {
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
if (PlatformExtension.isDesktop) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is InnerViewItem &&
widget.view.name == parentName &&
widget.view.layout == parentLayout,
skipOffstage: false,
),
matching: findPageName(name, layout: layout),
return find.byWidgetPredicate(
(widget) =>
widget is SingleMobileInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
@@ -11,9 +11,7 @@ class MockFilePicker implements FilePickerService {
final List<String> mockPaths;
@override
Future<String?> getDirectoryPath({String? title}) {
return Future.value(mockPath);
}
Future<String?> getDirectoryPath({String? title}) => Future.value(mockPath);
@override
Future<String?> saveFile({
@@ -23,9 +21,8 @@ class MockFilePicker implements FilePickerService {
FileType type = FileType.any,
List<String>? allowedExtensions,
bool lockParentWindow = false,
}) {
return Future.value(mockPath);
}
}) =>
Future.value(mockPath);
@override
Future<FilePickerResult?> pickFiles({
@@ -42,34 +39,21 @@ class MockFilePicker implements FilePickerService {
}) {
final platformFiles =
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
return Future.value(
FilePickerResult(
platformFiles,
),
);
return Future.value(FilePickerResult(platformFiles));
}
}
Future<void> mockGetDirectoryPath(
String path,
) async {
Future<void> mockGetDirectoryPath(String path) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: path,
),
() => MockFilePicker(mockPath: path),
);
return;
}
Future<String> mockSaveFilePath(
String path,
) async {
Future<String> mockSaveFilePath(String path) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: path,
),
() => MockFilePicker(mockPath: path),
);
return path;
}
@@ -77,9 +61,7 @@ Future<String> mockSaveFilePath(
List<String> mockPickFilePaths({required List<String> paths}) {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPaths: paths,
),
() => MockFilePicker(mockPaths: paths),
);
return paths;
}
@@ -1,10 +1,11 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:mocktail/mocktail.dart';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MyMockClient extends Mock implements http.Client {
@override
@@ -52,7 +53,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
final response = await client.send(request);
var previousSyntax = '';
String previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
@@ -76,6 +77,5 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
}
}
}
return;
}
}
@@ -27,9 +27,7 @@ class MockUrlLauncher extends Fake
bool launchCalled = false;
// ignore: use_setters_to_change_properties
void setCanLaunchExpectations(String url) {
this.url = url;
}
void setCanLaunchExpectations(String url) => this.url = url;
void setLaunchExpectations({
required String url,
@@ -53,10 +51,7 @@ class MockUrlLauncher extends Fake
this.webOnlyWindowName = webOnlyWindowName;
}
// ignore: use_setters_to_change_properties
void setResponse(bool response) {
this.response = response;
}
void setResponse(bool response) => this.response = response;
@override
LinkDelegate? get linkDelegate => null;
@@ -104,7 +99,5 @@ class MockUrlLauncher extends Fake
}
@override
Future<void> closeWebView() async {
closeWebViewCalled = true;
}
Future<void> closeWebView() async => closeWebViewCalled = true;
}
@@ -1,13 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
extension AppFlowySettings on WidgetTester {
@@ -31,14 +35,6 @@ extension AppFlowySettings on WidgetTester {
return;
}
Future<void> expectNoSettingsPage(SettingsPage page) async {
final button = find.byWidgetPredicate(
(widget) => widget is SettingsMenuElement && widget.page == page,
);
expect(button, findsNothing);
return;
}
/// Restore the AppFlowy data storage location
Future<void> restoreLocation() async {
final button =
@@ -48,13 +44,6 @@ extension AppFlowySettings on WidgetTester {
return;
}
Future<void> tapOpenFolderButton() async {
final button = find.text(LocaleKeys.settings_files_open.tr());
expect(button, findsOneWidget);
await tapButton(button);
return;
}
Future<void> tapCustomLocationButton() async {
final button = find.byTooltip(
LocaleKeys.settings_files_changeLocationTooltips.tr(),
@@ -66,12 +55,22 @@ extension AppFlowySettings on WidgetTester {
/// Enter user name
Future<void> enterUserName(String name) async {
final uni = find.byType(UserNameInput);
expect(uni, findsOneWidget);
await tap(uni);
await enterText(uni, name);
await wait(300); //
await testTextInput.receiveAction(TextInputAction.done);
// Enable editing username
final editUsernameFinder = find.descendant(
of: find.byType(UserProfileSetting),
matching: find.byFlowySvg(FlowySvgs.edit_s),
);
await tap(editUsernameFinder);
await pumpAndSettle();
final userNameFinder = find.descendant(
of: find.byType(UserProfileSetting),
matching: find.byType(FlowyTextField),
);
await enterText(userNameFinder, name);
await pumpAndSettle();
await tap(find.text(LocaleKeys.button_save.tr()));
await pumpAndSettle();
}
+10
View File
@@ -37,6 +37,16 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
# dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
]
end
end
installer.aggregate_targets.each do |target|
+10 -10
View File
@@ -63,9 +63,9 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- ReachabilitySwift (5.0.0)
- rich_clipboard_ios (0.0.1):
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
@@ -100,7 +100,7 @@ DEPENDENCIES:
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -147,8 +147,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
rich_clipboard_ios:
:path: ".symlinks/plugins/rich_clipboard_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -164,10 +164,10 @@ SPEC CHECKSUMS:
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
@@ -176,10 +176,10 @@ SPEC CHECKSUMS:
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
rich_clipboard_ios: 7588abe18f881a6d0e9ec0b12e51cae2761e8942
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
@@ -189,6 +189,6 @@ SPEC CHECKSUMS:
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
@@ -127,6 +127,7 @@
97C146EC1CF9000F007C117D /* Resources */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */,
A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -233,6 +234,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
A548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
E790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -1,18 +1,10 @@
import 'dart:typed_data';
import 'package:appflowy/core/notification/notification_helper.dart';
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
const String _source = 'Document';
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
FlowyResult<Uint8List, FlowyError>,
);
class DocumentNotificationParser
extends NotificationParser<DocumentNotification, FlowyError> {
DocumentNotificationParser({
@@ -12,12 +12,6 @@ import 'notification_helper.dart';
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
const String _source = 'Workspace';
// Folder
typedef FolderNotificationCallback = void Function(
FolderNotification,
FlowyResult<Uint8List, FlowyError>,
);
class FolderNotificationParser
extends NotificationParser<FolderNotification, FlowyError> {
FolderNotificationParser({
@@ -12,12 +12,6 @@ import 'notification_helper.dart';
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
const String _source = 'Database';
// DatabasePB
typedef DatabaseNotificationCallback = void Function(
DatabaseNotification,
FlowyResult<Uint8List, FlowyError>,
);
class DatabaseNotificationParser
extends NotificationParser<DatabaseNotification, FlowyError> {
DatabaseNotificationParser({
@@ -13,11 +13,6 @@ import 'notification_helper.dart';
// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)
const _source = 'Search';
typedef SearchNotificationCallback = void Function(
SearchNotification,
FlowyResult<Uint8List, FlowyError>,
);
class SearchNotificationParser
extends NotificationParser<SearchNotification, FlowyError> {
SearchNotificationParser({
@@ -1,23 +1,11 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// This value should be the same as the USER_OBSERVABLE_SOURCE value
const String _source = 'User';
// User
typedef UserNotificationCallback = void Function(
UserNotification,
FlowyResult<Uint8List, FlowyError>,
);
class UserNotificationParser
extends NotificationParser<UserNotification, FlowyError> {
UserNotificationParser({
@@ -29,26 +17,3 @@ class UserNotificationParser
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
typedef UserNotificationHandler = Function(
UserNotification ty,
FlowyResult<Uint8List, FlowyError> result,
);
class UserNotificationListener {
UserNotificationListener({
required String objectId,
required UserNotificationHandler handler,
}) : _parser = UserNotificationParser(id: objectId, callback: handler) {
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
UserNotificationParser? _parser;
StreamSubscription<SubscribeObject>? _subscription;
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
}
}
+2 -1
View File
@@ -319,7 +319,8 @@ Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
// Construct the WebSocket URL directly from the parsed URI.
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws/v1');
final wsUrl =
Uri(scheme: wsScheme, host: uri.host, port: uri.port, path: '/ws/v1');
return wsUrl.toString();
} catch (e) {
@@ -0,0 +1,99 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'mobile_view_page_bloc.freezed.dart';
class MobileViewPageBloc
extends Bloc<MobileViewPageEvent, MobileViewPageState> {
MobileViewPageBloc({
required this.viewId,
}) : _viewListener = ViewListener(viewId: viewId),
super(MobileViewPageState.initial()) {
on<MobileViewPageEvent>(
(event, emit) async {
await event.when(
initial: () async {
_registerListeners();
final result = await ViewBackendService.getView(viewId);
final isImmersiveMode =
_isImmersiveMode(result.fold((s) => s, (f) => null));
emit(
state.copyWith(
isLoading: false,
result: result,
isImmersiveMode: isImmersiveMode,
),
);
},
updateImmersionMode: (isImmersiveMode) {
emit(
state.copyWith(
isImmersiveMode: isImmersiveMode,
),
);
},
);
},
);
}
final String viewId;
final ViewListener _viewListener;
@override
Future<void> close() {
_viewListener.stop();
return super.close();
}
void _registerListeners() {
_viewListener.start(
onViewUpdated: (view) {
final isImmersiveMode = _isImmersiveMode(view);
add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode));
},
);
}
// only the document page supports immersive mode (version 0.5.6)
bool _isImmersiveMode(ViewPB? view) {
if (view == null) {
return false;
}
final cover = view.cover;
if (cover == null || cover.type == PageStyleCoverImageType.none) {
return false;
} else if (view.layout == ViewLayoutPB.Document) {
// only support immersive mode for document layout
return true;
}
return false;
}
}
@freezed
class MobileViewPageEvent with _$MobileViewPageEvent {
const factory MobileViewPageEvent.initial() = Initial;
const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) =
UpdateImmersionMode;
}
@freezed
class MobileViewPageState with _$MobileViewPageState {
const factory MobileViewPageState({
@Default(true) bool isLoading,
@Default(null) FlowyResult<ViewPB, FlowyError>? result,
@Default(false) bool isImmersiveMode,
}) = _MobileViewPageState;
factory MobileViewPageState.initial() => const MobileViewPageState();
}
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
@@ -19,6 +20,7 @@ extension MobileRouter on BuildContext {
queryParameters: view.queryParameters(arguments),
).toString(),
).then((value) {
getIt<MenuSharedState>().latestOpenView = view;
getIt<CachedRecentService>().updateRecentViews([view.id], true);
});
}
@@ -0,0 +1,431 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_page_style_bloc.freezed.dart';
class DocumentPageStyleBloc
extends Bloc<DocumentPageStyleEvent, DocumentPageStyleState> {
DocumentPageStyleBloc({
required this.view,
}) : super(DocumentPageStyleState.initial()) {
on<DocumentPageStyleEvent>(
(event, emit) async {
await event.when(
initial: () async {
try {
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
(f) => {},
);
final fontLayout = _getSelectedFontLayout(layoutObject);
final lineHeightLayout = _getSelectedLineHeightLayout(
layoutObject,
);
final fontFamily = _getSelectedFontFamily(layoutObject);
final cover = _getSelectedCover(layoutObject);
final coverType = cover.$1;
final coverValue = cover.$2;
emit(
state.copyWith(
fontLayout: fontLayout,
fontFamily: fontFamily,
lineHeightLayout: lineHeightLayout,
coverImage: PageStyleCover(
type: coverType,
value: coverValue,
),
iconPadding: calculateIconPadding(
fontLayout,
lineHeightLayout,
),
),
);
} catch (e) {
Log.error('Failed to decode layout object: $e');
}
},
updateFont: (fontLayout) async {
emit(
state.copyWith(
fontLayout: fontLayout,
iconPadding: calculateIconPadding(
fontLayout,
state.lineHeightLayout,
),
),
);
unawaited(updateLayoutObject());
},
updateLineHeight: (lineHeightLayout) async {
emit(
state.copyWith(
lineHeightLayout: lineHeightLayout,
iconPadding: calculateIconPadding(
state.fontLayout,
lineHeightLayout,
),
),
);
unawaited(updateLayoutObject());
},
updateFontFamily: (fontFamily) async {
emit(
state.copyWith(
fontFamily: fontFamily,
),
);
unawaited(updateLayoutObject());
},
updateCoverImage: (coverImage) async {
emit(
state.copyWith(
coverImage: coverImage,
),
);
unawaited(updateLayoutObject());
},
);
},
);
}
final ViewPB view;
final ViewBackendService viewBackendService = ViewBackendService();
Future<void> updateLayoutObject() async {
final layoutObject = decodeLayoutObject();
if (layoutObject != null) {
await ViewBackendService.updateView(
viewId: view.id,
extra: layoutObject,
);
}
}
String? decodeLayoutObject() {
Map oldValue = {};
try {
final extra = view.extra;
oldValue = jsonDecode(extra);
} catch (e) {
Log.error('Failed to decode layout object: $e');
}
final newValue = {
ViewExtKeys.fontLayoutKey: state.fontLayout.toString(),
ViewExtKeys.lineHeightLayoutKey: state.lineHeightLayout.toString(),
ViewExtKeys.coverKey: {
ViewExtKeys.coverTypeKey: state.coverImage.type.toString(),
ViewExtKeys.coverValueKey: state.coverImage.value,
},
ViewExtKeys.fontKey: state.fontFamily,
};
final merged = mergeMaps(oldValue, newValue);
return jsonEncode(merged);
}
// because the line height can not be calculated accurately,
// we need to adjust the icon padding manually.
double calculateIconPadding(
PageStyleFontLayout fontLayout,
PageStyleLineHeightLayout lineHeightLayout,
) {
double padding = switch (fontLayout) {
PageStyleFontLayout.small => 1.0,
PageStyleFontLayout.normal => 2.0,
PageStyleFontLayout.large => 4.0,
};
switch (lineHeightLayout) {
case PageStyleLineHeightLayout.small:
padding -= 1.0;
break;
case PageStyleLineHeightLayout.normal:
break;
case PageStyleLineHeightLayout.large:
padding += 3.0;
break;
}
return max(0, padding);
}
PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) {
final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ??
PageStyleFontLayout.normal.toString();
return PageStyleFontLayout.values.firstWhere(
(e) => e.toString() == fontLayout,
);
}
PageStyleLineHeightLayout _getSelectedLineHeightLayout(Map layoutObject) {
final lineHeightLayout = layoutObject[ViewExtKeys.lineHeightLayoutKey] ??
PageStyleLineHeightLayout.normal.toString();
return PageStyleLineHeightLayout.values.firstWhere(
(e) => e.toString() == lineHeightLayout,
);
}
String? _getSelectedFontFamily(Map layoutObject) {
return layoutObject[ViewExtKeys.fontKey];
}
(PageStyleCoverImageType, String colorValue) _getSelectedCover(
Map layoutObject,
) {
final cover = layoutObject[ViewExtKeys.coverKey] ?? {};
final coverType = cover[ViewExtKeys.coverTypeKey] ??
PageStyleCoverImageType.none.toString();
final coverValue = cover[ViewExtKeys.coverValueKey] ?? '';
return (
PageStyleCoverImageType.values.firstWhere(
(e) => e.toString() == coverType,
),
coverValue,
);
}
}
@freezed
class DocumentPageStyleEvent with _$DocumentPageStyleEvent {
const factory DocumentPageStyleEvent.initial() = Initial;
const factory DocumentPageStyleEvent.updateFont(
PageStyleFontLayout fontLayout,
) = UpdateFontSize;
const factory DocumentPageStyleEvent.updateLineHeight(
PageStyleLineHeightLayout lineHeightLayout,
) = UpdateLineHeight;
const factory DocumentPageStyleEvent.updateFontFamily(
String? fontFamily,
) = UpdateFontFamily;
const factory DocumentPageStyleEvent.updateCoverImage(
PageStyleCover coverImage,
) = UpdateCoverImage;
}
@freezed
class DocumentPageStyleState with _$DocumentPageStyleState {
const factory DocumentPageStyleState({
@Default(PageStyleFontLayout.normal) PageStyleFontLayout fontLayout,
@Default(PageStyleLineHeightLayout.normal)
PageStyleLineHeightLayout lineHeightLayout,
// the default font family is null, which means the system font
@Default(null) String? fontFamily,
@Default(2.0) double iconPadding,
required PageStyleCover coverImage,
}) = _DocumentPageStyleState;
factory DocumentPageStyleState.initial() => DocumentPageStyleState(
coverImage: PageStyleCover.none(),
);
}
enum PageStyleFontLayout {
small,
normal,
large;
@override
String toString() {
switch (this) {
case PageStyleFontLayout.small:
return 'small';
case PageStyleFontLayout.normal:
return 'normal';
case PageStyleFontLayout.large:
return 'large';
}
}
static PageStyleFontLayout fromString(String value) {
return PageStyleFontLayout.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleFontLayout.normal;
}
double get fontSize {
switch (this) {
case PageStyleFontLayout.small:
return 14.0;
case PageStyleFontLayout.normal:
return 16.0;
case PageStyleFontLayout.large:
return 18.0;
}
}
List<double> get headingFontSizes {
switch (this) {
case PageStyleFontLayout.small:
return [22.0, 18.0, 16.0, 16.0, 16.0, 16.0];
case PageStyleFontLayout.normal:
return [24.0, 20.0, 18.0, 18.0, 18.0, 18.0];
case PageStyleFontLayout.large:
return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];
}
}
double get factor {
switch (this) {
case PageStyleFontLayout.small:
return PageStyleFontLayout.small.fontSize /
PageStyleFontLayout.normal.fontSize;
case PageStyleFontLayout.normal:
return 1.0;
case PageStyleFontLayout.large:
return PageStyleFontLayout.large.fontSize /
PageStyleFontLayout.normal.fontSize;
}
}
}
enum PageStyleLineHeightLayout {
small,
normal,
large;
@override
String toString() {
switch (this) {
case PageStyleLineHeightLayout.small:
return 'small';
case PageStyleLineHeightLayout.normal:
return 'normal';
case PageStyleLineHeightLayout.large:
return 'large';
}
}
static PageStyleLineHeightLayout fromString(String value) {
return PageStyleLineHeightLayout.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleLineHeightLayout.normal;
}
double get lineHeight {
switch (this) {
case PageStyleLineHeightLayout.small:
return 1.4;
case PageStyleLineHeightLayout.normal:
return 1.5;
case PageStyleLineHeightLayout.large:
return 1.75;
}
}
double get padding {
switch (this) {
case PageStyleLineHeightLayout.small:
return 6.0;
case PageStyleLineHeightLayout.normal:
return 8.0;
case PageStyleLineHeightLayout.large:
return 8.0;
}
}
List<double> get headingPaddings {
switch (this) {
case PageStyleLineHeightLayout.small:
return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];
case PageStyleLineHeightLayout.normal:
return [30.0, 24.0, 22.0, 22.0, 22.0, 22.0];
case PageStyleLineHeightLayout.large:
return [34.0, 28.0, 26.0, 26.0, 26.0, 26.0];
}
}
}
// for the version above 0.5.5
enum PageStyleCoverImageType {
none,
// normal color
pureColor,
// gradient color
gradientColor,
// built in images
builtInImage,
// custom images, uploaded by the user
customImage,
// local image
localImage,
// unsplash images
unsplashImage;
@override
String toString() {
switch (this) {
case PageStyleCoverImageType.none:
return 'none';
case PageStyleCoverImageType.pureColor:
return 'color';
case PageStyleCoverImageType.gradientColor:
return 'gradient';
case PageStyleCoverImageType.builtInImage:
return 'built_in';
case PageStyleCoverImageType.customImage:
return 'custom';
case PageStyleCoverImageType.localImage:
return 'local';
case PageStyleCoverImageType.unsplashImage:
return 'unsplash';
}
}
static PageStyleCoverImageType fromString(String value) {
return PageStyleCoverImageType.values.firstWhereOrNull(
(e) => e.toString() == value,
) ??
PageStyleCoverImageType.none;
}
static String builtInImagePath(String value) {
return 'assets/images/built_in_cover_images/m_cover_image_$value.png';
}
}
class PageStyleCover {
const PageStyleCover({
required this.type,
required this.value,
});
factory PageStyleCover.none() => const PageStyleCover(
type: PageStyleCoverImageType.none,
value: '',
);
final PageStyleCoverImageType type;
// there're 4 types of values:
// 1. pure color: enum value
// 2. gradient color: enum value
// 3. built-in image: the image name, read from the assets
// 4. custom image or unsplash image: the image url
final String value;
bool get isPresets => isPureColor || isGradient || isBuiltInImage;
bool get isPhoto => isCustomImage || isLocalImage;
bool get isNone => type == PageStyleCoverImageType.none;
bool get isPureColor => type == PageStyleCoverImageType.pureColor;
bool get isGradient => type == PageStyleCoverImageType.gradientColor;
bool get isBuiltInImage => type == PageStyleCoverImageType.builtInImage;
bool get isCustomImage => type == PageStyleCoverImageType.customImage;
bool get isUnsplashImage => type == PageStyleCoverImageType.unsplashImage;
bool get isLocalImage => type == PageStyleCoverImageType.localImage;
}
@@ -1,8 +1,10 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_listener.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -21,10 +23,14 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
initial: () async {
_documentListener.start(
onDocEventUpdate: (docEvent) async {
final (coverType, coverValue) = await getCover();
if (state.coverTypeV2 != null) {
return;
}
final (coverType, coverValue) = await getCoverV1();
add(
RecentViewEvent.updateCover(
coverType,
null,
coverValue,
),
);
@@ -38,17 +44,40 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
view.icon.value,
),
);
if (view.extra.isNotEmpty) {
final cover = view.cover;
add(
RecentViewEvent.updateCover(
CoverType.none,
cover?.type,
cover?.value,
),
);
}
},
);
final (coverType, coverValue) = await getCover();
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverType: coverType,
coverValue: coverValue,
),
);
final cover = getCoverV2();
if (cover != null) {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverTypeV2: cover.type,
coverValue: cover.value,
),
);
} else {
final (coverTypeV1, coverValue) = await getCoverV1();
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
coverTypeV1: coverTypeV1,
coverValue: coverValue,
),
);
}
},
updateNameOrIcon: (name, icon) {
emit(
@@ -58,10 +87,11 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
),
);
},
updateCover: (coverType, coverValue) {
updateCover: (coverTypeV1, coverTypeV2, coverValue) {
emit(
state.copyWith(
coverType: coverType,
coverTypeV1: coverTypeV1,
coverTypeV2: coverTypeV2,
coverValue: coverValue,
),
);
@@ -76,8 +106,13 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
final DocumentListener _documentListener;
final ViewListener _viewListener;
Future<(CoverType, String?)> getCover() async {
final result = await _service.getDocument(viewId: view.id);
PageStyleCover? getCoverV2() {
return view.cover;
}
// for the version under 0.5.5
Future<(CoverType, String?)> getCoverV1() async {
final result = await _service.getDocument(documentId: view.id);
final document = result.fold((s) => s.toDocument(), (f) => null);
if (document != null) {
final coverType = CoverType.fromString(
@@ -102,7 +137,8 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
class RecentViewEvent with _$RecentViewEvent {
const factory RecentViewEvent.initial() = Initial;
const factory RecentViewEvent.updateCover(
CoverType coverType,
CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5
PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5
String? coverValue,
) = UpdateCover;
const factory RecentViewEvent.updateNameOrIcon(
@@ -116,7 +152,8 @@ class RecentViewState with _$RecentViewState {
const factory RecentViewState({
required String name,
required String icon,
@Default(CoverType.none) CoverType coverType,
@Default(CoverType.none) CoverType coverTypeV1,
PageStyleCoverImageType? coverTypeV2,
@Default(null) String? coverValue,
}) = _RecentViewState;
@@ -1,4 +1,4 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -37,9 +37,11 @@ class FlowyAppBar extends AppBar {
Widget? title,
String? titleText,
FlowyAppBarLeadingType leadingType = FlowyAppBarLeadingType.back,
Widget? leading,
super.centerTitle,
VoidCallback? onTapLeading,
bool showDivider = true,
super.backgroundColor,
}) : super(
title: title ??
FlowyText(
@@ -49,7 +51,7 @@ class FlowyAppBar extends AppBar {
),
titleSpacing: 0,
elevation: 0,
leading: leadingType.getWidget(onTapLeading),
leading: leading ?? leadingType.getWidget(onTapLeading),
leadingWidth: leadingType.width,
toolbarHeight: 44.0,
bottom: showDivider
@@ -17,7 +17,7 @@ class AppBarBackButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
padding: padding,
child: const FlowySvg(
FlowySvgs.m_app_bar_back_s,
@@ -37,7 +37,7 @@ class AppBarCloseButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
child: const FlowySvg(
FlowySvgs.m_app_bar_close_s,
),
@@ -56,7 +56,7 @@ class AppBarCancelButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
child: FlowyText(
LocaleKeys.button_cancel.tr(),
overflow: TextOverflow.ellipsis,
@@ -76,7 +76,7 @@ class AppBarDoneButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap,
onTap: (_) => onTap(),
padding: const EdgeInsets.all(12),
child: FlowyText(
LocaleKeys.button_done.tr(),
@@ -103,7 +103,7 @@ class AppBarSaveButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: () {
onTap: (_) {
if (enable) {
onTap();
}
@@ -166,7 +166,7 @@ class AppBarMoreButton extends StatelessWidget {
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.all(12),
onTap: () => onTap(context),
onTap: onTap,
child: const FlowySvg(FlowySvgs.three_dots_s),
);
}
@@ -180,7 +180,7 @@ class AppBarButton extends StatelessWidget {
this.padding,
});
final VoidCallback onTap;
final void Function(BuildContext context) onTap;
final Widget child;
final EdgeInsetsGeometry? padding;
@@ -188,7 +188,7 @@ class AppBarButton extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
onTap: () => onTap(context),
child: Padding(
padding: padding ?? const EdgeInsets.all(12),
child: child,
@@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
class FlowyBoxContainer extends StatelessWidget {
const FlowyBoxContainer({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 8.0,
),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.onSecondary,
),
borderRadius: BorderRadius.circular(8.0),
),
child: child,
);
}
}
@@ -1,11 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.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/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/shared/sync_indicator.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
@@ -13,15 +13,12 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.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';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPage extends StatefulWidget {
const MobileViewPage({
@@ -43,67 +40,33 @@ class MobileViewPage extends StatefulWidget {
}
class _MobileViewPageState extends State<MobileViewPage> {
late final Future<FlowyResult<ViewPB, FlowyError>> future;
// used to determine if the user has scrolled down and show the app bar in immersive mode
ScrollNotificationObserverState? _scrollNotificationObserver;
// control the app bar opacity when in immersive mode
final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);
@override
void initState() {
super.initState();
future = ViewBackendService.getView(widget.id);
void dispose() {
_appBarOpacity.dispose();
_scrollNotificationObserver = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, state) {
Widget body;
ViewPB? viewPB;
final actions = <Widget>[];
if (state.connectionState != ConnectionState.done) {
body = const Center(
child: CircularProgressIndicator(),
);
} else if (!state.hasData) {
body = FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: state.error.toString(),
);
} else {
body = state.data!.fold((view) {
viewPB = view;
actions.addAll([
if (FeatureFlag.syncDocument.isOn) ...[
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
view.layout == ViewLayoutPB.Document
? DocumentSyncIndicator(view: view)
: DatabaseSyncIndicator(view: view),
const HSpace(8.0),
],
_buildAppBarMoreButton(view),
]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
}, (error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
});
}
return BlocProvider(
create: (_) => MobileViewPageBloc(viewId: widget.id)
..add(const MobileViewPageEvent.initial()),
child: BlocBuilder<MobileViewPageBloc, MobileViewPageState>(
builder: (context, state) {
final view = state.result?.fold((s) => s, (f) => null);
final body = _buildBody(context, state);
if (view == null) {
return _buildApp(context, null, body);
}
if (viewPB != null) {
return MultiBlocProvider(
providers: [
BlocProvider(
@@ -112,120 +75,206 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
BlocProvider(
create: (_) =>
ViewBloc(view: viewPB!)..add(const ViewEvent.initial()),
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider.value(
value: getIt<ReminderBloc>()
..add(const ReminderEvent.started()),
),
if (view.layout.isDocumentView)
BlocProvider(
create: (_) => DocumentPageStyleBloc(view: view)
..add(const DocumentPageStyleEvent.initial()),
),
],
child: Builder(
builder: (context) {
final view = context.watch<ViewBloc>().state.view;
return _buildApp(view, actions, body);
return _buildApp(context, view, body);
},
),
);
} else {
return _buildApp(null, [], body);
}
},
);
}
Widget _buildApp(ViewPB? view, List<Widget> actions, Widget child) {
final icon = view?.icon.value;
return Scaffold(
appBar: FlowyAppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
Expanded(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
),
),
],
),
actions: actions,
},
),
body: SafeArea(child: child),
);
}
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarMoreButton(
onTap: (context) {
EditorNotification.exitEditing().post();
Widget _buildApp(
BuildContext context,
ViewPB? view,
Widget child,
) {
final isDocument = view?.layout.isDocumentView ?? false;
final title = _buildTitle(context, view);
final actions = _buildAppBarActions(context, view);
final appBar = isDocument
? MobileViewPageImmersiveAppBar(
preferredSize: Size(
double.infinity,
AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,
),
title: title,
appBarOpacity: _appBarOpacity,
actions: actions,
)
: FlowyAppBar(title: title, actions: actions);
final body = isDocument
? Builder(
builder: (context) {
_rebuildScrollNotificationObserver(context);
return child;
},
)
: child;
return Scaffold(
extendBodyBehindAppBar: isDocument,
appBar: appBar,
body: body,
);
}
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => _buildViewPageBottomSheet(context),
Widget _buildBody(BuildContext context, MobileViewPageState state) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
final result = state.result;
if (result == null) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: '',
);
}
return result.fold(
(view) {
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
},
(error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
title: LocaleKeys.error_weAreSorry.tr(),
description: LocaleKeys.error_loadingViewError.tr(),
errorMsg: error.toString(),
);
},
);
}
Widget _buildViewPageBottomSheet(BuildContext context) {
final view = context.read<ViewBloc>().state.view;
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
// Document:
// - [ collaborators, sync_indicator, layout_button, more_button]
// Database:
// - [ sync_indicator, more_button]
List<Widget> _buildAppBarActions(BuildContext context, ViewPB? view) {
if (view == null) {
return [];
}
final isImmersiveMode =
context.read<MobileViewPageBloc>().state.isImmersiveMode;
final actions = <Widget>[];
if (FeatureFlag.syncDocument.isOn) {
// only document supports displaying collaborators.
if (view.layout.isDocumentView) {
actions.addAll([
DocumentCollaborators(
width: 60,
height: 44,
fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 8),
view: view,
),
const HSpace(16.0),
DocumentSyncIndicator(view: view),
const HSpace(12.0),
]);
} else {
actions.addAll([
DatabaseSyncIndicator(view: view),
const HSpace(12.0),
]);
}
}
if (view.layout.isDocumentView) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
}
actions.addAll([
MobileViewPageMoreButton(
view: view,
isImmersiveMode: isImmersiveMode,
appBarOpacity: _appBarOpacity,
),
]);
return actions;
}
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon.value;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 34.0),
child: EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
),
Expanded(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
void _rebuildScrollNotificationObserver(BuildContext context) {
_scrollNotificationObserver?.removeListener(_onScrollNotification);
_scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
_scrollNotificationObserver?.addListener(_onScrollNotification);
}
// immersive mode related
// auto show or hide the app bar based on the scroll position
void _onScrollNotification(ScrollNotification notification) {
if (_scrollNotificationObserver == null) {
return;
}
if (notification is ScrollUpdateNotification &&
defaultScrollNotificationPredicate(notification)) {
final ScrollMetrics metrics = notification.metrics;
double height = MediaQuery.of(context).padding.top;
if (defaultTargetPlatform == TargetPlatform.android) {
height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight;
}
final progress = (metrics.pixels / height).clamp(0.0, 1.0);
// reduce the sensitivity of the app bar opacity change
if ((progress - _appBarOpacity.value).abs() >= 0.1 ||
progress == 0 ||
progress == 1.0) {
_appBarOpacity.value = progress;
}
}
}
}
@@ -0,0 +1,227 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageImmersiveAppBar extends StatelessWidget
implements PreferredSizeWidget {
const MobileViewPageImmersiveAppBar({
super.key,
required this.preferredSize,
required this.appBarOpacity,
required this.title,
required this.actions,
});
final ValueListenable appBarOpacity;
final Widget title;
final List<Widget> actions;
@override
final Size preferredSize;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0),
child: _buildAppBarBackButton(context),
),
actions: actions,
),
);
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
onTap: (context) => context.pop(),
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_back_s,
dimension: 30.0,
iconPadding: 6.0,
isImmersiveMode:
context.read<MobileViewPageBloc>().state.isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageMoreButton extends StatelessWidget {
const MobileViewPageMoreButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
return AppBarButton(
padding: const EdgeInsets.only(left: 8, right: 16),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<ViewBloc>()),
BlocProvider.value(value: context.read<FavoriteBloc>()),
],
child: MobileViewPageMoreBottomSheet(view: view),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_app_bar_more_s,
dimension: 30.0,
iconPadding: 5.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class MobileViewPageLayoutButton extends StatelessWidget {
const MobileViewPageLayoutButton({
super.key,
required this.view,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final ViewPB view;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
// only display the layout button if the view is a document
if (view.layout != ViewLayoutPB.Document) {
return const SizedBox.shrink();
}
return AppBarButton(
padding: const EdgeInsets.symmetric(vertical: 2.0),
onTap: (context) {
EditorNotification.exitEditing().post();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.pageStyle_title.tr(),
backgroundColor: Theme.of(context).colorScheme.background,
builder: (_) => BlocProvider.value(
value: context.read<DocumentPageStyleBloc>(),
child: PageStyleBottomSheet(
view: context.read<ViewBloc>().state.view,
),
),
);
},
child: _ImmersiveAppBarButton(
icon: FlowySvgs.m_layout_s,
dimension: 30.0,
iconPadding: 5.0,
isImmersiveMode: isImmersiveMode,
appBarOpacity: appBarOpacity,
),
);
}
}
class _ImmersiveAppBarButton extends StatelessWidget {
const _ImmersiveAppBarButton({
required this.icon,
required this.dimension,
required this.iconPadding,
required this.isImmersiveMode,
required this.appBarOpacity,
});
final FlowySvgData icon;
final double dimension;
final double iconPadding;
final bool isImmersiveMode;
final ValueListenable appBarOpacity;
@override
Widget build(BuildContext context) {
assert(
dimension > 0.0 && dimension <= kToolbarHeight,
'dimension must be greater than 0, and less than or equal to kToolbarHeight',
);
// if the immersive mode is on, the icon should be white and add a black background
// also, the icon opacity will change based on the app bar opacity
return UnconstrainedBox(
child: SizedBox.square(
dimension: dimension,
child: ValueListenableBuilder(
valueListenable: appBarOpacity,
builder: (context, appBarOpacity, child) {
Color? color;
// if there's no cover or the cover is not immersive,
// make sure the app bar is always visible
if (!isImmersiveMode) {
color = null;
} else if (appBarOpacity < 0.99) {
color = Colors.white;
}
Widget child = Container(
margin: EdgeInsets.all(iconPadding),
child: FlowySvg(icon, color: color),
);
if (isImmersiveMode && appBarOpacity <= 0.99) {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
}
return child;
},
),
),
);
}
}
@@ -0,0 +1,67 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileViewPageMoreBottomSheet extends StatelessWidget {
const MobileViewPageMoreBottomSheet({super.key, required this.view});
final ViewPB view;
@override
Widget build(BuildContext context) {
return ViewPageBottomSheet(
view: view,
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
// show toast
break;
case MobileViewBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.delete:
// pop to home page
context
..pop()
..pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewBottomSheetBodyAction.addToFavorites:
case MobileViewBottomSheetBodyAction.removeFromFavorites:
context.pop();
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
EditorNotification.redo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
},
onRename: (name) {
if (name != view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
@@ -55,6 +55,31 @@ class BottomSheetDoneButton extends StatelessWidget {
}
}
class BottomSheetRemoveButton extends StatelessWidget {
const BottomSheetRemoveButton({
super.key,
required this.onRemove,
});
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onRemove,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),
child: FlowyText(
LocaleKeys.button_remove.tr(),
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
textAlign: TextAlign.right,
),
),
);
}
}
class BottomSheetBackButton extends StatelessWidget {
const BottomSheetBackButton({
super.key,
@@ -32,6 +32,8 @@ Future<T?> showMobileBottomSheet<T>(
// this field is only used if showHeader is true
bool showBackButton = false,
bool showCloseButton = false,
bool showRemoveButton = false,
VoidCallback? onRemove,
// this field is only used if showHeader is true
String title = '',
bool isScrollControlled = true,
@@ -46,6 +48,8 @@ Future<T?> showMobileBottomSheet<T>(
double? elevation,
bool showDoneButton = false,
bool enableDraggableScrollable = false,
// this field is only used if showDragHandle is true
Widget Function(BuildContext, ScrollController)? scrollableWidgetBuilder,
// only used when enableDraggableScrollable is true
double minChildSize = 0.5,
double maxChildSize = 0.8,
@@ -102,7 +106,9 @@ Future<T?> showMobileBottomSheet<T>(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
showRemoveButton: showRemoveButton,
title: title,
onRemove: onRemove,
),
);
@@ -116,24 +122,30 @@ Future<T?> showMobileBottomSheet<T>(
// ----- header area -----
if (enableDraggableScrollable) {
final keyboardSize =
context.bottomSheetPadding() / MediaQuery.of(context).size.height;
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: initialChildSize,
minChildSize: minChildSize,
maxChildSize: maxChildSize,
initialChildSize: (initialChildSize + keyboardSize).clamp(0, 1),
minChildSize: (minChildSize + keyboardSize).clamp(0, 1.0),
maxChildSize: (maxChildSize + keyboardSize).clamp(0, 1.0),
builder: (context, scrollController) {
return Column(
children: [
...children,
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: child,
scrollableWidgetBuilder?.call(
context,
scrollController,
) ??
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
),
),
),
],
);
},
@@ -175,14 +187,18 @@ class BottomSheetHeader extends StatelessWidget {
super.key,
required this.showBackButton,
required this.showCloseButton,
required this.showRemoveButton,
required this.title,
required this.showDoneButton,
this.onRemove,
});
final bool showBackButton;
final bool showCloseButton;
final bool showRemoveButton;
final String title;
final bool showDoneButton;
final VoidCallback? onRemove;
@override
Widget build(BuildContext context) {
@@ -202,6 +218,13 @@ class BottomSheetHeader extends StatelessWidget {
alignment: Alignment.centerLeft,
child: BottomSheetCloseButton(),
),
if (showRemoveButton)
Align(
alignment: Alignment.centerLeft,
child: BottomSheetRemoveButton(
onRemove: () => onRemove?.call(),
),
),
Align(
child: FlowyText(
title,
@@ -66,6 +66,7 @@ Future<T?> showTransitionMobileBottomSheet<T>(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
showRemoveButton: false,
title: title,
),
if (showDivider)
@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Add new group
class MobileBoardTrailing extends StatefulWidget {
const MobileBoardTrailing({
super.key,
});
const MobileBoardTrailing({super.key});
@override
State<MobileBoardTrailing> createState() => _MobileBoardTrailingState();
@@ -49,13 +48,24 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
TextField(
controller: _textController,
autofocus: true,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
Icons.close,
color: style.colorScheme.onBackground,
suffixIcon: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _textController.text.isNotEmpty ? 1 : 0,
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: IconButton(
icon: Icon(
Icons.close,
color: style.colorScheme.onBackground,
),
onPressed: () =>
setState(() => _textController.clear()),
),
),
onPressed: () => _textController.clear(),
),
isDense: true,
),
@@ -66,9 +76,7 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
),
);
_textController.clear();
setState(() {
isEditing = false;
});
setState(() => isEditing = false);
},
),
Row(
@@ -81,11 +89,7 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
color: style.colorScheme.onBackground,
),
),
onPressed: () {
setState(() {
isEditing = false;
});
},
onPressed: () => setState(() => isEditing = false),
),
TextButton(
child: Text(
@@ -102,9 +106,7 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
),
);
_textController.clear();
setState(() {
isEditing = false;
});
setState(() => isEditing = false);
},
),
],
@@ -120,6 +122,9 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
).copyWith(
overlayColor:
MaterialStateProperty.all(Theme.of(context).hoverColor),
),
icon: const Icon(Icons.add),
label: Text(
@@ -128,9 +133,7 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
fontWeight: FontWeight.w600,
),
),
onPressed: () => setState(
() => isEditing = true,
),
onPressed: () => setState(() => isEditing = true),
),
),
);
@@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@@ -1,4 +1,4 @@
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_empty.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
@@ -8,7 +10,6 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
@@ -28,23 +29,15 @@ class MobileFavoritePageFolder extends StatelessWidget {
providers: [
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
userProfile,
workspaceId,
),
),
..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<FavoriteBloc>().add(
const FavoriteEvent.initial(),
);
},
listener: (context, state) =>
context.read<FavoriteBloc>().add(const FavoriteEvent.initial()),
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart';
@@ -5,7 +7,6 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileFavoriteFolder extends StatelessWidget {
@@ -62,9 +63,7 @@ class MobileFavoriteFolder extends StatelessWidget {
isFeedback: false,
view: view,
level: 0,
onSelected: (view) async {
await context.pushView(view);
},
onSelected: context.pushView,
endActionPane: (context) => buildEndActionPane(context, [
view.isFavorite
? MobilePaneActionType.removeFromFavorites
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -6,14 +8,13 @@ import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bott
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -1,7 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
@@ -24,16 +24,16 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RecentViewsBloc()
..add(
const RecentViewsEvent.initial(),
),
create: (context) =>
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<RecentViewsBloc>().add(
const RecentViewsEvent.fetchRecentViews(),
);
},
listenWhen: (previous, current) =>
current.currentWorkspace != null &&
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace!.workspaceId,
listener: (context, state) => context
.read<RecentViewsBloc>()
.add(const RecentViewsEvent.resetRecentViews()),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final ids = <String>{};
@@ -1,10 +1,13 @@
import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@@ -47,55 +50,14 @@ class MobileRecentView extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: _RecentCover(
coverType: state.coverType,
value: state.coverValue,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 18, 8, 2),
// hack: minLines currently not supported in Text widget.
// https://github.com/flutter/flutter/issues/31134
child: Stack(
children: [
FlowyText.medium(
view.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const FlowyText(
"\n\n",
maxLines: 2,
),
],
),
),
),
Expanded(child: _buildCover(context, state)),
Expanded(child: _buildTitle(context, state)),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: state.icon.isNotEmpty
? EmojiText(
emoji: state.icon,
fontSize: 30.0,
)
: SizedBox.square(
dimension: 32.0,
child: view.defaultIcon(),
),
),
child: _buildIcon(context, state),
),
],
),
@@ -104,15 +66,71 @@ class MobileRecentView extends StatelessWidget {
),
);
}
Widget _buildCover(BuildContext context, RecentViewState state) {
return Padding(
padding: const EdgeInsets.only(top: 1.0, left: 1.0, right: 1.0),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: _RecentCover(
coverTypeV1: state.coverTypeV1,
coverTypeV2: state.coverTypeV2,
value: state.coverValue,
),
),
);
}
Widget _buildTitle(BuildContext context, RecentViewState state) {
return Padding(
padding: const EdgeInsets.fromLTRB(8, 18, 8, 2),
// hack: minLines currently not supported in Text widget.
// https://github.com/flutter/flutter/issues/31134
child: Stack(
children: [
FlowyText.medium(
view.name,
fontSize: 16.0,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const FlowyText(
"\n\n",
maxLines: 2,
),
],
),
);
}
Widget _buildIcon(BuildContext context, RecentViewState state) {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: state.icon.isNotEmpty
? EmojiText(
emoji: state.icon,
fontSize: 30.0,
)
: SizedBox.square(
dimension: 32.0,
child: view.defaultIcon(),
),
);
}
}
class _RecentCover extends StatelessWidget {
const _RecentCover({
required this.coverType,
required this.coverTypeV1,
this.coverTypeV2,
this.value,
});
final CoverType coverType;
final CoverType coverTypeV1;
final PageStyleCoverImageType? coverTypeV2;
final String? value;
@override
@@ -125,7 +143,62 @@ class _RecentCover extends StatelessWidget {
if (value == null) {
return placeholder;
}
switch (coverType) {
if (coverTypeV2 != null) {
return _buildCoverV2(context, value, placeholder);
}
return _buildCoverV1(context, value, placeholder);
}
Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {
final type = coverTypeV2;
if (type == null) {
return placeholder;
}
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
final userProfilePB = Provider.of<UserProfilePB?>(context);
return FlowyNetworkImage(
url: value,
userProfilePB: userProfilePB,
);
}
if (type == PageStyleCoverImageType.builtInImage) {
return Image.asset(
PageStyleCoverImageType.builtInImagePath(value),
fit: BoxFit.cover,
);
}
if (type == PageStyleCoverImageType.pureColor) {
final color = value.coverColor(context);
if (color != null) {
return ColoredBox(
color: color,
);
}
}
if (type == PageStyleCoverImageType.gradientColor) {
return Container(
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
return Image.file(
File(value),
fit: BoxFit.cover,
);
}
return placeholder;
}
Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) {
switch (coverTypeV1) {
case CoverType.file:
if (isURL(value)) {
final userProfilePB = Provider.of<UserProfilePB?>(context);
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
@@ -9,7 +11,6 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSectionFolder extends StatelessWidget {
@@ -71,9 +72,7 @@ class MobileSectionFolder extends StatelessWidget {
level: 0,
leftPadding: 16,
isFeedback: false,
onSelected: (view) async {
await context.pushView(view);
},
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(context, [
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -10,7 +12,6 @@ import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_it
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -1,17 +1,21 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
final List<String> _availableFonts = GoogleFonts.asMap().keys.toList();
final List<String> _availableFonts = [
defaultFontFamily,
...GoogleFonts.asMap().keys,
];
class FontPickerScreen extends StatelessWidget {
const FontPickerScreen({super.key});
@@ -25,7 +29,9 @@ class FontPickerScreen extends StatelessWidget {
}
class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({super.key});
const LanguagePickerPage({
super.key,
});
@override
State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@@ -51,47 +57,93 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
),
body: SafeArea(
child: Scrollbar(
child: ListView.builder(
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) {
if (index == 0) {
// search bar
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
setState(() {
availableFonts = _availableFonts
.where(
(font) => font
.parseFontFamilyName()
.toLowerCase()
.contains(keyword.toLowerCase()),
)
.toList();
});
},
),
);
}
final fontFamilyName = availableFonts[index - 1];
final displayName = fontFamilyName.parseFontFamilyName();
return FlowyOptionTile.checkbox(
text: displayName,
isSelected: selectedFontFamilyName == fontFamilyName,
showTopBorder: false,
onTap: () => context.pop(fontFamilyName),
fontFamily: GoogleFonts.getFont(fontFamilyName).fontFamily,
backgroundColor: Colors.transparent,
);
},
child: FontSelector(
selectedFontFamilyName: selectedFontFamilyName,
onFontFamilySelected: (fontFamilyName) =>
context.pop(fontFamilyName),
),
),
),
);
}
}
class FontSelector extends StatefulWidget {
const FontSelector({
super.key,
this.scrollController,
required this.selectedFontFamilyName,
required this.onFontFamilySelected,
});
final ScrollController? scrollController;
final String selectedFontFamilyName;
final void Function(String fontFamilyName) onFontFamilySelected;
@override
State<FontSelector> createState() => _FontSelectorState();
}
class _FontSelectorState extends State<FontSelector> {
late List<String> availableFonts;
@override
void initState() {
super.initState();
availableFonts = _availableFonts;
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: widget.scrollController,
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) {
if (index == 0) {
// search bar
return _buildSearchBar(context);
}
final fontFamilyName = availableFonts[index - 1];
final usingDefaultFontFamily = fontFamilyName == defaultFontFamily;
final fontFamily = !usingDefaultFontFamily
? getGoogleFontSafely(fontFamilyName).fontFamily
: defaultFontFamily;
return FlowyOptionTile.checkbox(
text: fontFamilyName.fontFamilyDisplayName,
isSelected: widget.selectedFontFamilyName == fontFamilyName,
showTopBorder: false,
onTap: () => widget.onFontFamilySelected(fontFamilyName),
fontFamily: fontFamily,
backgroundColor: Colors.transparent,
);
},
);
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
setState(() {
availableFonts = _availableFonts
.where(
(font) =>
font.isEmpty || // keep the default one always
font
.parseFontFamilyName()
.toLowerCase()
.contains(keyword.toLowerCase()),
)
.toList();
});
},
),
);
}
}
@@ -1,16 +1,15 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../setting.dart';
@@ -23,14 +22,14 @@ class FontSetting extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final selectedFont = context.watch<AppearanceSettingsCubit>().state.font;
final name = selectedFont.fontFamilyDisplayName;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_fontFamily_label.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
selectedFont,
fontFamily: GoogleFonts.getFont(selectedFont).fontFamily,
name,
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/language.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/self_host_setting_group.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileQuickActionButton extends StatelessWidget {
const MobileQuickActionButton({
@@ -25,11 +26,7 @@ class MobileQuickActionButton extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: () {
if (enable) {
onTap();
}
},
onTap: enable ? onTap : null,
borderRadius: BorderRadius.circular(12),
overlayColor:
enable ? null : const MaterialStatePropertyAll(Colors.transparent),
@@ -26,7 +26,7 @@ class FlowyMobileSearchTextField extends StatelessWidget {
onSubmitted: onSubmitted,
placeholder: hintText,
prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
prefixInsets: const EdgeInsets.only(left: 16.0),
prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0),
suffixIcon: const Icon(Icons.close),
suffixInsets: const EdgeInsets.only(right: 16.0),
placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith(
@@ -34,6 +34,11 @@ class FlowyMobileSearchTextField extends StatelessWidget {
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
),
);
}
@@ -1,53 +1,86 @@
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';
///show the dialog to confirm one single action
///[onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog
enum ConfirmDialogActionAlignment {
// The action buttons are aligned vertically
// ---------------------
// | Action Button |
// | Cancel Button |
vertical,
// The action buttons are aligned horizontally
// ---------------------
// | Action Button | Cancel Button |
horizontal,
}
/// show the dialog to confirm one single action
/// [onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog
Future<T?> showFlowyMobileConfirmDialog<T>(
BuildContext context, {
Widget? title,
Widget? content,
ConfirmDialogActionAlignment actionAlignment =
ConfirmDialogActionAlignment.horizontal,
required String actionButtonTitle,
required VoidCallback? onActionButtonPressed,
Color? actionButtonColor,
String? cancelButtonTitle,
required void Function()? onActionButtonPressed,
void Function()? onCancelButtonPressed,
Color? cancelButtonColor,
VoidCallback? onCancelButtonPressed,
}) async {
return showDialog(
context: context,
builder: (dialogContext) {
final foregroundColor = Theme.of(context).colorScheme.onSurface;
final actionButton = TextButton(
child: FlowyText(
actionButtonTitle,
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();
},
);
final cancelButton = TextButton(
child: FlowyText(
cancelButtonTitle ?? LocaleKeys.button_cancel.tr(),
color: cancelButtonColor ?? foregroundColor,
),
onPressed: () {
onCancelButtonPressed?.call();
Navigator.of(dialogContext).pop();
},
);
final actions = switch (actionAlignment) {
ConfirmDialogActionAlignment.horizontal => [
actionButton,
cancelButton,
],
ConfirmDialogActionAlignment.vertical => [
Column(
children: [
actionButton,
const Divider(height: 1, color: Colors.grey),
cancelButton,
],
),
],
};
return AlertDialog.adaptive(
title: title,
content: 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();
},
),
],
contentPadding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 4.0,
),
actionsAlignment: MainAxisAlignment.center,
actions: actions,
);
},
);
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/color/color_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -9,7 +9,7 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:google_fonts/google_fonts.dart';
// use a global value to store the selected emoji to prevent reloading every time.
EmojiData? _cachedEmojiData;
EmojiData? kCachedEmojiData;
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
@@ -34,12 +34,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
super.initState();
// load the emoji data from cache if it's available
if (_cachedEmojiData != null) {
emojiData = _cachedEmojiData;
if (kCachedEmojiData != null) {
emojiData = kCachedEmojiData;
} else {
EmojiData.builtIn().then(
(value) {
_cachedEmojiData = value;
kCachedEmojiData = value;
setState(() {
emojiData = value;
});
@@ -1,41 +0,0 @@
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();
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -0,0 +1,111 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_cell_bloc.freezed.dart';
class SummaryCellBloc extends Bloc<SummaryCellEvent, SummaryCellState> {
SummaryCellBloc({
required this.cellController,
}) : super(SummaryCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final SummaryCellController cellController;
void Function()? _onCellChangedFn;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
await cellController.dispose();
return super.close();
}
void _dispatch() {
on<SummaryCellEvent>(
(event, emit) async {
await event.when(
didReceiveCellUpdate: (cellData) {
emit(
state.copyWith(content: cellData ?? ""),
);
},
didUpdateField: (fieldInfo) {
final wrap = fieldInfo.wrapCellContent;
if (wrap != null) {
emit(state.copyWith(wrap: wrap));
}
},
updateCell: (text) async {
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
SummaryCellEvent.didReceiveCellUpdate(
cellController.getCellData() ?? "",
),
);
}
},
);
},
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(
SummaryCellEvent.didReceiveCellUpdate(cellContent ?? ""),
);
}
},
onFieldChanged: _onFieldChangedListener,
);
}
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(SummaryCellEvent.didUpdateField(fieldInfo));
}
}
}
@freezed
class SummaryCellEvent with _$SummaryCellEvent {
const factory SummaryCellEvent.didReceiveCellUpdate(String? cellContent) =
_DidReceiveCellUpdate;
const factory SummaryCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
const factory SummaryCellEvent.updateCell(String text) = _UpdateCell;
}
@freezed
class SummaryCellState with _$SummaryCellState {
const factory SummaryCellState({
required String content,
required bool wrap,
}) = _SummaryCellState;
factory SummaryCellState.initial(SummaryCellController cellController) {
final wrap = cellController.fieldInfo.wrapCellContent;
return SummaryCellState(
content: cellController.getCellData() ?? "",
wrap: wrap ?? true,
);
}
}
@@ -0,0 +1,99 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_row_bloc.freezed.dart';
class SummaryRowBloc extends Bloc<SummaryRowEvent, SummaryRowState> {
SummaryRowBloc({
required this.viewId,
required this.rowId,
required this.fieldId,
}) : super(SummaryRowState.initial()) {
_dispatch();
}
final String viewId;
final String rowId;
final String fieldId;
void _dispatch() {
on<SummaryRowEvent>(
(event, emit) async {
event.when(
startSummary: () {
final params = SummaryRowPB(
viewId: viewId,
rowId: rowId,
fieldId: fieldId,
);
emit(
state.copyWith(
loadingState: const LoadingState.loading(),
error: null,
),
);
DatabaseEventSummarizeRow(params).send().then(
(result) => {
if (!isClosed) add(SummaryRowEvent.finishSummary(result)),
},
);
},
finishSummary: (result) {
result.fold(
(s) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: null,
),
),
},
(err) => {
emit(
state.copyWith(
loadingState: const LoadingState.finish(),
error: err,
),
),
},
);
},
);
},
);
}
}
@freezed
class SummaryRowEvent with _$SummaryRowEvent {
const factory SummaryRowEvent.startSummary() = _DidStartSummary;
const factory SummaryRowEvent.finishSummary(
FlowyResult<void, FlowyError> result,
) = _DidFinishSummary;
}
@freezed
class SummaryRowState with _$SummaryRowState {
const factory SummaryRowState({
required LoadingState loadingState,
required FlowyError? error,
}) = _SummaryRowState;
factory SummaryRowState.initial() {
return const SummaryRowState(
loadingState: LoadingState.finish(),
error: null,
);
}
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
}
@@ -105,7 +105,7 @@ class TextCellState with _$TextCellState {
factory TextCellState.initial(TextCellController cellController) {
final cellData = cellController.getCellData() ?? "";
final wrap = cellController.fieldInfo.wrapCellContent ?? false;
final wrap = cellController.fieldInfo.wrapCellContent ?? true;
final emoji =
cellController.fieldInfo.isPrimary ? cellController.icon ?? "" : "";
@@ -68,6 +68,8 @@ class CellController<T, D> {
Timer? _loadDataOperation;
Timer? _saveDataOperation;
Completer? _completer;
RowId get rowId => _cellContext.rowId;
String get fieldId => _cellContext.fieldId;
FieldInfo get fieldInfo => _fieldController.getField(_cellContext.fieldId)!;
@@ -192,6 +194,7 @@ class CellController<T, D> {
_loadDataOperation?.cancel();
if (debounce) {
_saveDataOperation?.cancel();
_completer = Completer();
_saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
final result = await _cellDataPersistence.save(
viewId: viewId,
@@ -199,6 +202,7 @@ class CellController<T, D> {
data: data,
);
onFinish?.call(result);
_completer?.complete();
});
} else {
final result = await _cellDataPersistence.save(
@@ -241,6 +245,7 @@ class CellController<T, D> {
);
_loadDataOperation?.cancel();
await _completer?.future;
_saveDataOperation?.cancel();
_cellDataNotifier?.dispose();
_cellDataNotifier = null;
@@ -15,6 +15,7 @@ typedef DateCellController = CellController<DateCellDataPB, String>;
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
typedef RelationCellController = CellController<RelationCellDataPB, String>;
typedef SummaryCellController = CellController<String, String>;
CellController makeCellController(
DatabaseController databaseController,
@@ -132,6 +133,18 @@ CellController makeCellController(
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Summary:
return SummaryCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
}
throw UnimplementedError;
}
@@ -6,11 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'cell_controller.dart';
abstract class IGridCellDataConfig {
// The cell data will reload if it receives the field's change notification.
bool get reloadOnFieldChanged;
}
abstract class CellDataParser<T> {
T? parserData(List<int> data);
}
@@ -56,8 +51,13 @@ class CellDataLoader<T> {
class StringCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
final s = utf8.decode(data);
return s;
try {
final s = utf8.decode(data);
return s;
} catch (e) {
Log.error("Failed to parse string data: $e");
return null;
}
}
}
@@ -67,14 +67,25 @@ class CheckboxCellDataParser implements CellDataParser<CheckboxCellDataPB> {
if (data.isEmpty) {
return null;
}
return CheckboxCellDataPB.fromBuffer(data);
try {
return CheckboxCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse checkbox data: $e");
return null;
}
}
}
class NumberCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
return utf8.decode(data);
try {
return utf8.decode(data);
} catch (e) {
Log.error("Failed to parse number data: $e");
return null;
}
}
}
@@ -84,7 +95,12 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
if (data.isEmpty) {
return null;
}
return DateCellDataPB.fromBuffer(data);
try {
return DateCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse date data: $e");
return null;
}
}
}
@@ -94,7 +110,12 @@ class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
if (data.isEmpty) {
return null;
}
return TimestampCellDataPB.fromBuffer(data);
try {
return TimestampCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse timestamp data: $e");
return null;
}
}
}
@@ -105,7 +126,12 @@ class SelectOptionCellDataParser
if (data.isEmpty) {
return null;
}
return SelectOptionCellDataPB.fromBuffer(data);
try {
return SelectOptionCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse select option data: $e");
return null;
}
}
}
@@ -115,7 +141,13 @@ class ChecklistCellDataParser implements CellDataParser<ChecklistCellDataPB> {
if (data.isEmpty) {
return null;
}
return ChecklistCellDataPB.fromBuffer(data);
try {
return ChecklistCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse checklist data: $e");
return null;
}
}
}
@@ -125,13 +157,27 @@ class URLCellDataParser implements CellDataParser<URLCellDataPB> {
if (data.isEmpty) {
return null;
}
return URLCellDataPB.fromBuffer(data);
try {
return URLCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse url data: $e");
return null;
}
}
}
class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
@override
RelationCellDataPB? parserData(List<int> data) {
return data.isEmpty ? null : RelationCellDataPB.fromBuffer(data);
if (data.isEmpty) {
return null;
}
try {
return RelationCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse relation data: $e");
return null;
}
}
}
Loaded 100 of 797 files, more files were not shown because too many files have changed in this diff. Show more