Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05603c9472 | ||
|
|
e28d463cb9 | ||
|
|
bdc66103c8 | ||
|
|
e2b6e14762 | ||
|
|
6220680ce0 | ||
|
|
b4279f8004 | ||
|
|
dc813d85a8 | ||
|
|
aa4fe2ba50 | ||
|
|
a5b5a1f679 | ||
|
|
287c69a4fb | ||
|
|
c4f6703b76 | ||
|
|
3bdc8c222c | ||
|
|
560529f7d9 | ||
|
|
9f44ae959a | ||
|
|
b2c366f6ae | ||
|
|
3a9de61599 | ||
|
|
266a2a53ab | ||
|
|
d52042fa4f | ||
|
|
a69e83c2cb | ||
|
|
999ffeba21 | ||
|
|
cb15018825 | ||
|
|
22e89fd197 | ||
|
|
554c821db9 | ||
|
|
2eb88821ca | ||
|
|
813d024810 | ||
|
|
401ce7dfea | ||
|
|
cadb177acd | ||
|
|
016e131bd3 | ||
|
|
7831d8d4ab | ||
|
|
e1e8747f15 | ||
|
|
6bfac6b80a | ||
|
|
a971f3c6d2 | ||
|
|
1d73174b0c | ||
|
|
10a1571910 | ||
|
|
4d860cc404 | ||
|
|
9a8109f5f8 | ||
|
|
4981baac13 | ||
|
|
f3544375c9 | ||
|
|
33802fa62d | ||
|
|
6d0598b101 | ||
|
|
e0d6b194bf | ||
|
|
9135fb94ad | ||
|
|
969726ef73 | ||
|
|
3c446d5e78 | ||
|
|
3fce5eebf8 | ||
|
|
119fb03342 | ||
|
|
0b94e2b25d | ||
|
|
044dad1d3e | ||
|
|
d4278a7549 | ||
|
|
d467dcd68a | ||
|
|
1ad351195d | ||
|
|
122a392bf8 | ||
|
|
d86afb03ba | ||
|
|
cc66147bc0 |
No files matched your search
@@ -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 }}
|
||||
@@ -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,4 +1,4 @@
|
||||
name: Tauri2-CI
|
||||
name: Tauri-CI
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
-1
@@ -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
-2
@@ -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);
|
||||
|
||||
+3
-48
@@ -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": "}"},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+6
-3
@@ -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';
|
||||
|
||||
+139
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
+431
@@ -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;
|
||||
|
||||
|
||||
+4
-2
@@ -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
|
||||
+8
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
@@ -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;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+67
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -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,
|
||||
|
||||
+33
-10
@@ -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,
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ Future<T?> showTransitionMobileBottomSheet<T>(
|
||||
showCloseButton: showCloseButton,
|
||||
showBackButton: showBackButton,
|
||||
showDoneButton: showDoneButton,
|
||||
showRemoveButton: false,
|
||||
title: title,
|
||||
),
|
||||
if (showDivider)
|
||||
|
||||
+26
-23
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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';
|
||||
|
||||
+5
-12
@@ -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>(
|
||||
|
||||
+3
-4
@@ -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
-1
@@ -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';
|
||||
|
||||
+9
-9
@@ -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>{};
|
||||
|
||||
+120
-47
@@ -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);
|
||||
|
||||
+3
-4
@@ -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
-1
@@ -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';
|
||||
|
||||
+96
-44
@@ -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
-1
@@ -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';
|
||||
|
||||
+3
-6
@@ -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),
|
||||
|
||||
+6
-1
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+64
-31
@@ -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';
|
||||
|
||||
|
||||
+111
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+99
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
+61
-15
@@ -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
Reference in new issue
Block a user