Merge branch 'AppFlowy-IO:main' into main

This commit is contained in:
Simon 2024-08-21 14:28:12 +02:00 committed by GitHub
commit 5a3b5e0542
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
360 changed files with 8134 additions and 7833 deletions

View File

@ -20,34 +20,34 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
tauri-build-self-hosted: # tauri-build-self-hosted:
if: github.event.pull_request.head.repo.full_name == github.repository # if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted # runs-on: self-hosted
#
steps: # steps:
- uses: actions/checkout@v4 # - uses: actions/checkout@v4
- name: install frontend dependencies # - name: install frontend dependencies
working-directory: frontend/appflowy_web_app # working-directory: frontend/appflowy_web_app
run: | # run: |
mkdir dist # mkdir dist
pnpm install # pnpm install
cd src-tauri && cargo build # cd src-tauri && cargo build
#
- name: test and lint # - name: test and lint
working-directory: frontend/appflowy_web_app # working-directory: frontend/appflowy_web_app
run: | # run: |
pnpm run lint:tauri # pnpm run lint:tauri
#
- uses: tauri-apps/tauri-action@v0 # - uses: tauri-apps/tauri-action@v0
env: # env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: # with:
tauriScript: pnpm tauri # tauriScript: pnpm tauri
projectPath: frontend/appflowy_web_app # projectPath: frontend/appflowy_web_app
args: "--debug" # args: "--debug"
tauri-build-ubuntu: tauri-build-ubuntu:
if: github.event.pull_request.head.repo.full_name != github.repository #if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/black" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground
android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome
android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -0,0 +1,12 @@
# dart_dependency_validator.yaml
allow_pins: true
include:
- "lib/**"
exclude:
- "packages/**"
ignore:
- analyzer

View File

@ -1,93 +1,93 @@
import 'package:appflowy/env/cloud_env.dart'; // import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/workspace/application/settings/prelude.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/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; // import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:flutter_test/flutter_test.dart'; // import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; // import 'package:integration_test/integration_test.dart';
import '../shared/util.dart'; // import '../shared/util.dart';
void main() { // void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('supabase auth', () { // group('supabase auth', () {
testWidgets('sign in with supabase', (tester) async { // testWidgets('sign in with supabase', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); // await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton(); // await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
}); // });
testWidgets('sign out with supabase', (tester) async { // testWidgets('sign out with supabase', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); // await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton(); // await tester.tapGoogleLoginInButton();
// Open the setting page and sign out // // Open the setting page and sign out
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account); // await tester.openSettingsPage(SettingsPage.account);
await tester.logout(); // await tester.logout();
// Go to the sign in page again // // Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1)); // await tester.pumpAndSettle(const Duration(seconds: 1));
tester.expectToSeeGoogleLoginButton(); // tester.expectToSeeGoogleLoginButton();
}); // });
testWidgets('sign in as anonymous', (tester) async { // testWidgets('sign in as anonymous', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); // await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapSignInAsGuest(); // await tester.tapSignInAsGuest();
// should not see the sync setting page when sign in as anonymous // // should not see the sync setting page when sign in as anonymous
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account); // await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out // // Scroll to sign-out
await tester.scrollUntilVisible( // await tester.scrollUntilVisible(
find.byType(SignInOutButton), // find.byType(SignInOutButton),
100, // 100,
scrollable: find.findSettingsScrollable(), // scrollable: find.findSettingsScrollable(),
); // );
await tester.tapButton(find.byType(SignInOutButton)); // await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton(); // tester.expectToSeeGoogleLoginButton();
}); // });
// testWidgets('enable encryption', (tester) async { // // testWidgets('enable encryption', (tester) async {
// await tester.initializeAppFlowy(cloudType: CloudType.supabase); // // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// await tester.tapGoogleLoginInButton(); // // await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out // // // Open the setting page and sign out
// await tester.openSettings(); // // await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.cloud); // // await tester.openSettingsPage(SettingsPage.cloud);
// // the switch should be off by default // // // the switch should be off by default
// tester.assertEnableEncryptSwitchValue(false); // // tester.assertEnableEncryptSwitchValue(false);
// await tester.toggleEnableEncrypt(); // // await tester.toggleEnableEncrypt();
// // the switch should be on after toggling // // // the switch should be on after toggling
// tester.assertEnableEncryptSwitchValue(true); // // tester.assertEnableEncryptSwitchValue(true);
// // the switch can not be toggled back to off // // // the switch can not be toggled back to off
// await tester.toggleEnableEncrypt(); // // await tester.toggleEnableEncrypt();
// tester.assertEnableEncryptSwitchValue(true); // // tester.assertEnableEncryptSwitchValue(true);
// }); // // });
testWidgets('enable sync', (tester) async { // testWidgets('enable sync', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); // await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton(); // await tester.tapGoogleLoginInButton();
// Open the setting page and sign out // // Open the setting page and sign out
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.cloud); // await tester.openSettingsPage(SettingsPage.cloud);
// the switch should be on by default // // the switch should be on by default
tester.assertSupabaseEnableSyncSwitchValue(true); // tester.assertSupabaseEnableSyncSwitchValue(true);
await tester.toggleEnableSync(SupabaseEnableSync); // await tester.toggleEnableSync(SupabaseEnableSync);
// the switch should be off // // the switch should be off
tester.assertSupabaseEnableSyncSwitchValue(false); // tester.assertSupabaseEnableSyncSwitchValue(false);
// the switch should be on after toggling // // the switch should be on after toggling
await tester.toggleEnableSync(SupabaseEnableSync); // await tester.toggleEnableSync(SupabaseEnableSync);
tester.assertSupabaseEnableSyncSwitchValue(true); // tester.assertSupabaseEnableSyncSwitchValue(true);
}); // });
}); // });
} // }

View File

@ -47,31 +47,28 @@ void main() {
await tester.openSettingsPage(SettingsPage.account); await tester.openSettingsPage(SettingsPage.account);
await tester.enterUserName(name); await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
await tester.pumpAndSettle(const Duration(seconds: 6)); await tester.pumpAndSettle(const Duration(seconds: 6));
}); await tester.logout();
await tester.pumpAndSettle(const Duration(seconds: 2));
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
}); });
}); });
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
});
} }

View File

@ -461,22 +461,22 @@ void main() {
tester.assertChecklistEditorVisible(visible: true); tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter // create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true); await tester.createNewChecklistTask(name: "task 1", enter: true);
// assert that the task is displayed // assert that the task is displayed
tester.assertChecklistTaskInEditor( tester.assertChecklistTaskInEditor(
index: 0, index: 0,
name: "task 0", name: "task 1",
isChecked: false, isChecked: false,
); );
// update the task's name // update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1"); await tester.renameChecklistTask(index: 0, name: "task 11");
// assert that the task's name is updated // assert that the task's name is updated
tester.assertChecklistTaskInEditor( tester.assertChecklistTaskInEditor(
index: 0, index: 0,
name: "task 1", name: "task 11",
isChecked: false, isChecked: false,
); );

View File

@ -176,6 +176,7 @@ Future<void> createInlineDatabase(
await tester.editor.showSlashMenu(); await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName( await tester.editor.tapSlashMenuItemWithName(
layout.slashMenuName, layout.slashMenuName,
offset: 100,
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@ -2,7 +2,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.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/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -52,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester {
assert(isSwitched == value); assert(isSwitched == value);
} }
void assertEnableEncryptSwitchValue(bool value) {
assertSwitchValue(
find.descendant(
of: find.byType(EnableEncrypt),
matching: find.byWidgetPredicate((widget) => widget is Switch),
),
value,
);
}
void assertSupabaseEnableSyncSwitchValue(bool value) {
assertSwitchValue(
find.descendant(
of: find.byType(SupabaseEnableSync),
matching: find.byWidgetPredicate((widget) => widget is Switch),
),
value,
);
}
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) { void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
assertToggleValue( assertToggleValue(
find.descendant( find.descendant(
@ -82,15 +61,6 @@ extension AppFlowyAuthTest on WidgetTester {
); );
} }
Future<void> toggleEnableEncrypt() async {
final finder = find.descendant(
of: find.byType(EnableEncrypt),
matching: find.byWidgetPredicate((widget) => widget is Switch),
);
await tapButton(finder);
}
Future<void> toggleEnableSync(Type syncButton) async { Future<void> toggleEnableSync(Type syncButton) async {
final finder = find.descendant( final finder = find.descendant(
of: find.byType(syncButton), of: find.byType(syncButton),

View File

@ -7,7 +7,6 @@ import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.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/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.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/settings/prelude.dart';
@ -55,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester {
switch (cloudType) { switch (cloudType) {
case AuthenticatorType.local: case AuthenticatorType.local:
break; break;
case AuthenticatorType.supabase:
break;
case AuthenticatorType.appflowyCloudSelfHost: case AuthenticatorType.appflowyCloudSelfHost:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com"; rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password"; rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
@ -75,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester {
case AuthenticatorType.local: case AuthenticatorType.local:
await useLocalServer(); await useLocalServer();
break; break;
case AuthenticatorType.supabase:
await useTestSupabaseCloud();
getIt.unregister<AuthService>();
getIt.registerFactory<AuthService>(
() => SupabaseMockAuthService(),
);
break;
case AuthenticatorType.appflowyCloudSelfHost: case AuthenticatorType.appflowyCloudSelfHost:
await useTestSelfHostedAppFlowyCloud(); await useTestSelfHostedAppFlowyCloud();
getIt.unregister<AuthService>(); getIt.unregister<AuthService>();
@ -242,13 +232,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
} }
} }
Future<void> useTestSupabaseCloud() async {
await useSupabaseCloud(
url: TestEnv.supabaseUrl,
anonKey: TestEnv.supabaseAnonKey,
);
}
Future<void> useTestSelfHostedAppFlowyCloud() async { Future<void> useTestSelfHostedAppFlowyCloud() async {
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl); await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
} }

View File

@ -80,7 +80,7 @@ extension AppFlowySettings on WidgetTester {
of: find.byType(UserProfileSetting), of: find.byType(UserProfileSetting),
matching: find.byFlowySvg(FlowySvgs.edit_s), matching: find.byFlowySvg(FlowySvgs.edit_s),
); );
await tap(editUsernameFinder); await tap(editUsernameFinder, warnIfMissed: false);
await pumpAndSettle(); await pumpAndSettle();
final userNameFinder = find.descendant( final userNameFinder = find.descendant(

View File

@ -63,12 +63,15 @@ PODS:
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- printing (1.0.0):
- Flutter
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2): - SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2) - SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2) - SDWebImage/Core (5.14.2)
- Sentry/HybridSDK (8.33.0)
- sentry_flutter (8.7.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.33.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -100,7 +103,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- printing (from `.symlinks/plugins/printing/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`)
@ -113,6 +116,7 @@ SPEC REPOS:
- DKPhotoGallery - DKPhotoGallery
- ReachabilitySwift - ReachabilitySwift
- SDWebImage - SDWebImage
- Sentry
- SwiftyGif - SwiftyGif
- Toast - Toast
@ -147,8 +151,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
printing: sentry_flutter:
:path: ".symlinks/plugins/printing/ios" :path: ".symlinks/plugins/sentry_flutter/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@ -170,7 +174,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
@ -178,9 +182,10 @@ SPEC CHECKSUMS:
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
@ -191,4 +196,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2 COCOAPODS: 1.11.3

View File

@ -13,7 +13,6 @@ class AppFlowyConfiguration {
required this.device_id, required this.device_id,
required this.platform, required this.platform,
required this.authenticator_type, required this.authenticator_type,
required this.supabase_config,
required this.appflowy_cloud_config, required this.appflowy_cloud_config,
required this.envs, required this.envs,
}); });
@ -28,41 +27,12 @@ class AppFlowyConfiguration {
final String device_id; final String device_id;
final String platform; final String platform;
final int authenticator_type; final int authenticator_type;
final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config; final AppFlowyCloudConfiguration appflowy_cloud_config;
final Map<String, String> envs; final Map<String, String> envs;
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this); Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
} }
@JsonSerializable()
class SupabaseConfiguration {
SupabaseConfiguration({
required this.url,
required this.anon_key,
});
factory SupabaseConfiguration.fromJson(Map<String, dynamic> json) =>
_$SupabaseConfigurationFromJson(json);
/// Indicates whether the sync feature is enabled.
final String url;
final String anon_key;
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
static SupabaseConfiguration defaultConfig() {
return SupabaseConfiguration(
url: '',
anon_key: '',
);
}
bool get isValid {
return url.isNotEmpty && anon_key.isNotEmpty;
}
}
@JsonSerializable() @JsonSerializable()
class AppFlowyCloudConfiguration { class AppFlowyCloudConfiguration {
AppFlowyCloudConfiguration({ AppFlowyCloudConfiguration({

View File

@ -21,9 +21,6 @@ Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
case AuthenticatorType.local: case AuthenticatorType.local:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString()); await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
break; break;
case AuthenticatorType.supabase:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
break;
case AuthenticatorType.appflowyCloud: case AuthenticatorType.appflowyCloud:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString()); await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
break; break;
@ -63,8 +60,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
switch (value ?? "0") { switch (value ?? "0") {
case "0": case "0":
return AuthenticatorType.local; return AuthenticatorType.local;
case "1":
return AuthenticatorType.supabase;
case "2": case "2":
return AuthenticatorType.appflowyCloud; return AuthenticatorType.appflowyCloud;
case "3": case "3":
@ -93,10 +88,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
/// Returns `false` otherwise. /// Returns `false` otherwise.
bool get isAuthEnabled { bool get isAuthEnabled {
final env = getIt<AppFlowyCloudSharedEnv>(); final env = getIt<AppFlowyCloudSharedEnv>();
if (env.authenticatorType == AuthenticatorType.supabase) {
return env.supabaseConfig.isValid;
}
if (env.authenticatorType.isAppFlowyCloudEnabled) { if (env.authenticatorType.isAppFlowyCloudEnabled) {
return env.appflowyCloudConfig.isValid; return env.appflowyCloudConfig.isValid;
} }
@ -104,19 +95,6 @@ bool get isAuthEnabled {
return false; return false;
} }
/// Checks if Supabase is enabled.
///
/// This getter evaluates if Supabase should be enabled based on the
/// current integration mode and cloud type setting.
///
/// Returns:
/// A boolean value indicating whether Supabase is enabled. It returns `true`
/// if the application is in release or develop mode and the current cloud type
/// is `CloudType.supabase`. Otherwise, it returns `false`.
bool get isSupabaseEnabled {
return currentCloudType().isSupabaseEnabled;
}
/// Determines if AppFlowy Cloud is enabled. /// Determines if AppFlowy Cloud is enabled.
bool get isAppFlowyCloudEnabled { bool get isAppFlowyCloudEnabled {
return currentCloudType().isAppFlowyCloudEnabled; return currentCloudType().isAppFlowyCloudEnabled;
@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled {
enum AuthenticatorType { enum AuthenticatorType {
local, local,
supabase,
appflowyCloud, appflowyCloud,
appflowyCloudSelfHost, appflowyCloudSelfHost,
// The 'appflowyCloudDevelop' type is used for develop purposes only. // The 'appflowyCloudDevelop' type is used for develop purposes only.
@ -137,14 +114,10 @@ enum AuthenticatorType {
this == AuthenticatorType.appflowyCloudDevelop || this == AuthenticatorType.appflowyCloudDevelop ||
this == AuthenticatorType.appflowyCloud; this == AuthenticatorType.appflowyCloud;
bool get isSupabaseEnabled => this == AuthenticatorType.supabase;
int get value { int get value {
switch (this) { switch (this) {
case AuthenticatorType.local: case AuthenticatorType.local:
return 0; return 0;
case AuthenticatorType.supabase:
return 1;
case AuthenticatorType.appflowyCloud: case AuthenticatorType.appflowyCloud:
return 2; return 2;
case AuthenticatorType.appflowyCloudSelfHost: case AuthenticatorType.appflowyCloudSelfHost:
@ -158,8 +131,6 @@ enum AuthenticatorType {
switch (value) { switch (value) {
case 0: case 0:
return AuthenticatorType.local; return AuthenticatorType.local;
case 1:
return AuthenticatorType.supabase;
case 2: case 2:
return AuthenticatorType.appflowyCloud; return AuthenticatorType.appflowyCloud;
case 3: case 3:
@ -197,25 +168,15 @@ Future<void> useLocalServer() async {
await _setAuthenticatorType(AuthenticatorType.local); await _setAuthenticatorType(AuthenticatorType.local);
} }
Future<void> useSupabaseCloud({
required String url,
required String anonKey,
}) async {
await _setAuthenticatorType(AuthenticatorType.supabase);
await setSupabaseServer(url, anonKey);
}
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment. /// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
class AppFlowyCloudSharedEnv { class AppFlowyCloudSharedEnv {
AppFlowyCloudSharedEnv({ AppFlowyCloudSharedEnv({
required AuthenticatorType authenticatorType, required AuthenticatorType authenticatorType,
required this.appflowyCloudConfig, required this.appflowyCloudConfig,
required this.supabaseConfig,
}) : _authenticatorType = authenticatorType; }) : _authenticatorType = authenticatorType;
final AuthenticatorType _authenticatorType; final AuthenticatorType _authenticatorType;
final AppFlowyCloudConfiguration appflowyCloudConfig; final AppFlowyCloudConfiguration appflowyCloudConfig;
final SupabaseConfiguration supabaseConfig;
AuthenticatorType get authenticatorType => _authenticatorType; AuthenticatorType get authenticatorType => _authenticatorType;
@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv {
? await getAppFlowyCloudConfig(authenticatorType) ? await getAppFlowyCloudConfig(authenticatorType)
: AppFlowyCloudConfiguration.defaultConfig(); : AppFlowyCloudConfiguration.defaultConfig();
final supabaseCloudConfig = authenticatorType.isSupabaseEnabled
? await getSupabaseCloudConfig()
: SupabaseConfiguration.defaultConfig();
// In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend, // In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud]. // we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be // When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv( return AppFlowyCloudSharedEnv(
authenticatorType: authenticatorType, authenticatorType: authenticatorType,
appflowyCloudConfig: appflowyCloudConfig, appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: supabaseCloudConfig,
); );
} else { } else {
// Using the cloud settings from the .env file. // Using the cloud settings from the .env file.
@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv( return AppFlowyCloudSharedEnv(
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType), authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
appflowyCloudConfig: appflowyCloudConfig, appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: SupabaseConfiguration.defaultConfig(),
); );
} }
} }
@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv {
@override @override
String toString() { String toString() {
return 'authenticator: $_authenticatorType\n' return 'authenticator: $_authenticatorType\n'
'appflowy: ${appflowyCloudConfig.toJson()}\n' 'appflowy: ${appflowyCloudConfig.toJson()}\n';
'supabase: ${supabaseConfig.toJson()})\n';
} }
} }
@ -354,22 +308,3 @@ Future<void> setSupabaseServer(
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey); await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
} }
} }
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
final url = await _getSupabaseUrl();
final anonKey = await _getSupabaseAnonKey();
return SupabaseConfiguration(
url: url,
anon_key: anonKey,
);
}
Future<String> _getSupabaseUrl() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
return result ?? '';
}
Future<String> _getSupabaseAnonKey() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
return result ?? '';
}

View File

@ -36,4 +36,11 @@ abstract class Env {
defaultValue: '', defaultValue: '',
) )
static const String internalBuild = _Env.internalBuild; static const String internalBuild = _Env.internalBuild;
@EnviedField(
obfuscate: false,
varName: 'SENTRY_DSN',
defaultValue: '',
)
static const String sentryDsn = _Env.sentryDsn;
} }

View File

@ -2,27 +2,41 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart'; import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
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'; import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
extension MobileRouter on BuildContext { extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async { Future<void> pushView(
ViewPB view, {
Map<String, dynamic>? arguments,
bool addInRecent = true,
bool showMoreButton = true,
String? fixedTitle,
}) async {
// set the current view before pushing the new view // set the current view before pushing the new view
getIt<MenuSharedState>().latestOpenView = view; getIt<MenuSharedState>().latestOpenView = view;
unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true)); unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));
final queryParameters = view.queryParameters(arguments);
if (view.layout == ViewLayoutPB.Document) {
queryParameters[MobileDocumentScreen.viewShowMoreButton] =
showMoreButton.toString();
if (fixedTitle != null) {
queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
}
}
final uri = Uri( final uri = Uri(
path: view.routeName, path: view.routeName,
queryParameters: view.queryParameters(arguments), queryParameters: queryParameters,
).toString(); ).toString();
await push(uri); await push(uri);
} }

View File

@ -1,7 +1,5 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; 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_listener.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -113,7 +111,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
); );
} }
final _service = DocumentService();
final ViewPB view; final ViewPB view;
final DocumentListener _documentListener; final DocumentListener _documentListener;
final ViewListener _viewListener; final ViewListener _viewListener;
@ -124,16 +121,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
// for the version under 0.5.5 // for the version under 0.5.5
Future<(CoverType, String?)> getCoverV1() async { 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(
document.root.attributes[DocumentHeaderBlockKeys.coverType],
);
final coverValue = document
.root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
return (coverType, coverValue);
}
return (CoverType.none, null); return (CoverType.none, null);
} }

View File

@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
UserProfileBloc() : super(const _Initial()) { UserProfileBloc() : super(const _Initial()) {
on<UserProfileEvent>((event, emit) async { on<UserProfileEvent>((event, emit) async {
await event.when( await event.when(
started: () async => _initalize(emit), started: () async => _initialize(emit),
); );
}); });
} }
Future<void> _initalize(Emitter<UserProfileState> emit) async { Future<void> _initialize(Emitter<UserProfileState> emit) async {
emit(const UserProfileState.loading()); emit(const UserProfileState.loading());
final workspaceOrFailure = final workspaceOrFailure =

View File

@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType {
Widget getWidget(VoidCallback? onTap) { Widget getWidget(VoidCallback? onTap) {
switch (this) { switch (this) {
case FlowyAppBarLeadingType.back: case FlowyAppBarLeadingType.back:
return AppBarBackButton(onTap: onTap); return AppBarImmersiveBackButton(onTap: onTap);
case FlowyAppBarLeadingType.close: case FlowyAppBarLeadingType.close:
return AppBarCloseButton(onTap: onTap); return AppBarCloseButton(onTap: onTap);
case FlowyAppBarLeadingType.cancel: case FlowyAppBarLeadingType.cancel:

View File

@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget {
} }
} }
class AppBarImmersiveBackButton extends StatelessWidget {
const AppBarImmersiveBackButton({
super.key,
this.onTap,
});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
padding: const EdgeInsets.only(
left: 12.0,
top: 8.0,
bottom: 8.0,
right: 4.0,
),
child: const FlowySvg(
FlowySvgs.m_app_bar_back_s,
),
);
}
}
class AppBarCloseButton extends StatelessWidget { class AppBarCloseButton extends StatelessWidget {
const AppBarCloseButton({ const AppBarCloseButton({
super.key, super.key,

View File

@ -3,8 +3,8 @@ 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/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.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.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/document_collaborators.dart';
import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
@ -27,6 +27,8 @@ class MobileViewPage extends StatefulWidget {
required this.viewLayout, required this.viewLayout,
this.title, this.title,
this.arguments, this.arguments,
this.fixedTitle,
this.showMoreButton = true,
}); });
/// view id /// view id
@ -34,6 +36,10 @@ class MobileViewPage extends StatefulWidget {
final ViewLayoutPB viewLayout; final ViewLayoutPB viewLayout;
final String? title; final String? title;
final Map<String, dynamic>? arguments; final Map<String, dynamic>? arguments;
final bool showMoreButton;
// only used in row page
final String? fixedTitle;
@override @override
State<MobileViewPage> createState() => _MobileViewPageState(); State<MobileViewPage> createState() => _MobileViewPageState();
@ -164,6 +170,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
return plugin.widgetBuilder.buildWidget( return plugin.widgetBuilder.buildWidget(
shrinkWrap: false, shrinkWrap: false,
context: PluginContext(userProfile: state.userProfilePB), context: PluginContext(userProfile: state.userProfilePB),
data: {
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
},
); );
}, },
(error) { (error) {
@ -216,13 +225,19 @@ class _MobileViewPageState extends State<MobileViewPage> {
]); ]);
} }
actions.addAll([ if (widget.showMoreButton) {
MobileViewPageMoreButton( actions.addAll([
view: view, MobileViewPageMoreButton(
isImmersiveMode: isImmersiveMode, view: view,
appBarOpacity: _appBarOpacity, isImmersiveMode: isImmersiveMode,
), appBarOpacity: _appBarOpacity,
]); ),
]);
} else {
actions.addAll([
const HSpace(18.0),
]);
}
return actions; return actions;
} }
@ -232,19 +247,20 @@ class _MobileViewPageState extends State<MobileViewPage> {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (icon != null && icon.isNotEmpty) if (icon != null && icon.isNotEmpty) ...[
ConstrainedBox( FlowyText.emoji(
constraints: const BoxConstraints.tightFor(width: 34.0), icon,
child: EmojiText( fontSize: 15.0,
emoji: '$icon ', figmaLineHeight: 18.0,
fontSize: 22.0,
),
), ),
const HSpace(4),
],
Expanded( Expanded(
child: FlowyText.medium( child: FlowyText.medium(
view?.name ?? widget.title ?? '', widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0, fontSize: 15.0,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
), ),
), ),
], ],

View File

@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState
height: 42.0, height: 42.0,
child: FlowyTextField( child: FlowyTextField(
controller: controller, controller: controller,
textStyle: Theme.of(context).textTheme.bodyMedium,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
onSubmitted: (text) => widget.onRename(text), onSubmitted: (text) => widget.onRename(text),
), ),

View File

@ -116,12 +116,18 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async { Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
await showFlowyCupertinoConfirmDialog( await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.sideBar_removePageFromRecent.tr(), title: LocaleKeys.sideBar_removePageFromRecent.tr(),
leftButton: FlowyText.regular( leftButton: FlowyText(
LocaleKeys.button_cancel.tr(), LocaleKeys.button_cancel.tr(),
color: const Color(0xFF1456F0), fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
), ),
rightButton: FlowyText.medium( rightButton: FlowyText(
LocaleKeys.button_delete.tr(), LocaleKeys.button_delete.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220), color: const Color(0xFFFE0220),
), ),
onRightButtonPressed: (context) { onRightButtonPressed: (context) {

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.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/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.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'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@ -294,6 +295,7 @@ class MobileRowDetailPageContentState
RowCache get rowCache => widget.databaseController.rowCache; RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController => FieldController get fieldController =>
widget.databaseController.fieldController; widget.databaseController.fieldController;
ValueNotifier<String> primaryFieldId = ValueNotifier('');
@override @override
void initState() { void initState() {
@ -326,7 +328,13 @@ class MobileRowDetailPageContentState
fieldController: fieldController, fieldController: fieldController,
rowMeta: rowController.rowMeta, rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()), )..add(const RowBannerEvent.initial()),
child: BlocBuilder<RowBannerBloc, RowBannerState>( child: BlocConsumer<RowBannerBloc, RowBannerState>(
listener: (context, state) {
if (state.primaryField == null) {
return;
}
primaryFieldId.value = state.primaryField!.id;
},
builder: (context, state) { builder: (context, state) {
if (state.primaryField == null) { if (state.primaryField == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -366,6 +374,23 @@ class MobileRowDetailPageContentState
if (rowDetailState.numHiddenFields != 0) ...[ if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(), const ToggleHiddenFieldsVisibilityButton(),
], ],
const VSpace(8.0),
ValueListenableBuilder(
valueListenable: primaryFieldId,
builder: (context, primaryFieldId, child) {
if (primaryFieldId.isEmpty) {
return const SizedBox.shrink();
}
return OpenRowPageButton(
databaseController: widget.databaseController,
cellContext: CellContext(
rowId: rowController.rowId,
fieldId: primaryFieldId,
),
documentId: rowController.rowMeta.documentId,
);
},
),
MobileRowDetailCreateFieldButton( MobileRowDetailCreateFieldButton(
viewId: viewId, viewId: viewId,
fieldController: fieldController, fieldController: fieldController,

View File

@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: double.infinity, minWidth: double.infinity,
minHeight: GridSize.headerHeight, maxHeight: GridSize.headerHeight,
), ),
child: TextButton.icon( child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith( style: Theme.of(context).textButtonTheme.style?.copyWith(
@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory, splashFactory: NoSplash.splashFactory,
padding: const WidgetStatePropertyAll( padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(vertical: 14, horizontal: 6), EdgeInsets.symmetric(horizontal: 6, vertical: 2),
), ),
), ),
label: FlowyText.medium( label: FlowyText.medium(

View File

@ -0,0 +1,143 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OpenRowPageButton extends StatefulWidget {
const OpenRowPageButton({
super.key,
required this.documentId,
required this.databaseController,
required this.cellContext,
});
final String documentId;
final DatabaseController databaseController;
final CellContext cellContext;
@override
State<OpenRowPageButton> createState() => _OpenRowPageButtonState();
}
class _OpenRowPageButtonState extends State<OpenRowPageButton> {
late final cellBloc = TextCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
ViewPB? view;
@override
void initState() {
super.initState();
_preloadView(context, createDocumentIfMissed: true);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TextCellBloc, TextCellState>(
bloc: cellBloc,
builder: (context, state) {
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
maxHeight: GridSize.buttonHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
overlayColor: WidgetStateProperty.all<Color>(
Theme.of(context).hoverColor,
),
alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory,
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 6),
),
),
label: FlowyText.medium(
LocaleKeys.grid_field_openRowDocument.tr(),
fontSize: 15,
),
icon: const Padding(
padding: EdgeInsets.all(4.0),
child: FlowySvg(
FlowySvgs.full_view_s,
size: Size.square(16.0),
),
),
onPressed: () {
final name = state.content;
_openRowPage(context, name);
},
),
);
},
);
}
Future<void> _openRowPage(BuildContext context, String fieldName) async {
Log.info('Open row page(${widget.documentId})');
if (view == null) {
showToastNotification(context, message: 'Failed to open row page');
// reload the view again
unawaited(_preloadView(context));
Log.error('Failed to open row page(${widget.documentId})');
return;
}
if (context.mounted) {
// the document in row is an orphan document, so we don't add it to recent
await context.pushView(
view!,
addInRecent: false,
showMoreButton: false,
fixedTitle: fieldName,
);
}
}
// preload view to reduce the time to open the view
Future<void> _preloadView(
BuildContext context, {
bool createDocumentIfMissed = false,
}) async {
Log.info('Preload row page(${widget.documentId})');
final result = await ViewBackendService.getView(widget.documentId);
view = result.fold((s) => s, (f) => null);
if (view == null && createDocumentIfMissed) {
// create view if not exists
Log.info('Create row page(${widget.documentId})');
final result = await ViewBackendService.createOrphanView(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
viewId: widget.documentId,
layoutType: ViewLayoutPB.Document,
);
view = result.fold((s) => s, (f) => null);
}
}
}

View File

@ -7,15 +7,21 @@ class MobileDocumentScreen extends StatelessWidget {
super.key, super.key,
required this.id, required this.id,
this.title, this.title,
this.showMoreButton = true,
this.fixedTitle,
}); });
/// view id /// view id
final String id; final String id;
final String? title; final String? title;
final bool showMoreButton;
final String? fixedTitle;
static const routeName = '/docs'; static const routeName = '/docs';
static const viewId = 'id'; static const viewId = 'id';
static const viewTitle = 'title'; static const viewTitle = 'title';
static const viewShowMoreButton = 'show_more_button';
static const viewFixedTitle = 'fixed_title';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget {
id: id, id: id,
title: title, title: title,
viewLayout: ViewLayoutPB.Document, viewLayout: ViewLayoutPB.Document,
showMoreButton: showMoreButton,
fixedTitle: fixedTitle,
); );
} }
} }

View File

@ -96,36 +96,34 @@ class _FavoriteViews extends StatelessWidget {
final borderColor = Theme.of(context).isLightMode final borderColor = Theme.of(context).isLightMode
? const Color(0xFFE9E9EC) ? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF); : const Color(0x1AFFFFFF);
return Scrollbar( return ListView.separated(
child: ListView.separated( key: const PageStorageKey('favorite_views_page_storage_key'),
key: const PageStorageKey('favorite_views_page_storage_key'), padding: EdgeInsets.only(
padding: EdgeInsets.only( bottom: HomeSpaceViewSizes.mVerticalPadding +
bottom: HomeSpaceViewSizes.mVerticalPadding + MediaQuery.of(context).padding.bottom,
MediaQuery.of(context).padding.bottom, ),
), itemBuilder: (context, index) {
itemBuilder: (context, index) { final view = favoriteViews[index];
final view = favoriteViews[index]; return Container(
return Container( padding: const EdgeInsets.symmetric(vertical: 24.0),
padding: const EdgeInsets.symmetric(vertical: 24.0), decoration: BoxDecoration(
decoration: BoxDecoration( border: Border(
border: Border( bottom: BorderSide(
bottom: BorderSide( color: borderColor,
color: borderColor, width: 0.5,
width: 0.5,
),
), ),
), ),
child: MobileViewPage( ),
key: ValueKey(view.item.id), child: MobileViewPage(
view: view.item, key: ValueKey(view.item.id),
timestamp: view.timestamp, view: view.item,
type: MobilePageCardType.favorite, timestamp: view.timestamp,
), type: MobilePageCardType.favorite,
); ),
}, );
separatorBuilder: (context, index) => const HSpace(8), },
itemCount: favoriteViews.length, separatorBuilder: (context, index) => const HSpace(8),
), itemCount: favoriteViews.length,
); );
} }
} }

View File

@ -25,19 +25,17 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
final workspaceId = final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ?? context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
''; '';
return Scrollbar( return SingleChildScrollView(
child: SingleChildScrollView( child: Padding(
child: Padding( padding: EdgeInsets.only(
padding: EdgeInsets.only( top: HomeSpaceViewSizes.mVerticalPadding,
top: HomeSpaceViewSizes.mVerticalPadding, bottom: HomeSpaceViewSizes.mVerticalPadding +
bottom: HomeSpaceViewSizes.mVerticalPadding + MediaQuery.of(context).padding.bottom,
MediaQuery.of(context).padding.bottom, ),
), child: MobileFolders(
child: MobileFolders( user: widget.userProfile,
user: widget.userProfile, workspaceId: workspaceId,
workspaceId: workspaceId, showFavorite: false,
showFavorite: false,
),
), ),
), ),
); );

View File

@ -35,6 +35,9 @@ class MobileFolders extends StatelessWidget {
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ?? context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
''; '';
return BlocListener<UserWorkspaceBloc, UserWorkspaceState>( return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listenWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
listener: (context, state) { listener: (context, state) {
context.read<SidebarSectionsBloc>().add( context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.initial( SidebarSectionsEvent.initial(

View File

@ -1,8 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
@ -14,14 +16,19 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry/sentry.dart';
import 'package:toastification/toastification.dart';
class MobileHomeScreen extends StatelessWidget { class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key}); const MobileHomeScreen({super.key});
@ -59,6 +66,14 @@ class MobileHomeScreen extends StatelessWidget {
return const WorkspaceFailedScreen(); return const WorkspaceFailedScreen();
} }
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: userProfile.id.toString(),
),
),
);
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
bottom: false, bottom: false,
@ -94,6 +109,8 @@ class MobileHomePage extends StatefulWidget {
} }
class _MobileHomePageState extends State<MobileHomePage> { class _MobileHomePageState extends State<MobileHomePage> {
Loading? loadingIndicator;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -125,75 +142,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()), value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
), ),
], ],
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>( child: _HomePage(userProfile: widget.userProfile),
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
listener: (context, state) {
getIt<CachedRecentService>().reset();
mCurrentWorkspace.value = state.currentWorkspace;
},
builder: (context, state) {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
final workspaceId = state.currentWorkspace!.workspaceId;
return Column(
children: [
// Header
Padding(
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: 8.0,
top: Platform.isAndroid ? 8.0 : 0.0,
),
child: MobileHomePageHeader(
userProfile: widget.userProfile,
),
),
Expanded(
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SpaceOrderBloc()
..add(const SpaceOrderEvent.initial()),
),
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
widget.userProfile,
workspaceId,
),
),
),
BlocProvider(
create: (_) =>
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) => SpaceBloc()
..add(
SpaceEvent.initial(
widget.userProfile,
workspaceId,
openFirstPage: false,
),
),
),
],
child: MobileSpaceTab(
userProfile: widget.userProfile,
),
),
),
],
);
},
),
); );
} }
@ -205,3 +154,147 @@ class _MobileHomePageState extends State<MobileHomePage> {
await FolderEventSetLatestView(ViewIdPB(value: id)).send(); await FolderEventSetLatestView(ViewIdPB(value: id)).send();
} }
} }
class _HomePage extends StatefulWidget {
const _HomePage({required this.userProfile});
final UserProfilePB userProfile;
@override
State<_HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<_HomePage> {
Loading? loadingIndicator;
@override
Widget build(BuildContext context) {
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
listener: (context, state) {
getIt<CachedRecentService>().reset();
mCurrentWorkspace.value = state.currentWorkspace;
_showResultDialog(context, state);
},
builder: (context, state) {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
final workspaceId = state.currentWorkspace!.workspaceId;
return Column(
key: ValueKey('mobile_home_page_$workspaceId'),
children: [
// Header
Padding(
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: 8.0,
top: Platform.isAndroid ? 8.0 : 0.0,
),
child: MobileHomePageHeader(
userProfile: widget.userProfile,
),
),
Expanded(
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (_) =>
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
),
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(
SidebarSectionsEvent.initial(
widget.userProfile,
workspaceId,
),
),
),
BlocProvider(
create: (_) =>
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
BlocProvider(
create: (_) => SpaceBloc()
..add(
SpaceEvent.initial(
widget.userProfile,
workspaceId,
openFirstPage: false,
),
),
),
],
child: MobileSpaceTab(
userProfile: widget.userProfile,
),
),
),
],
);
},
);
}
void _showResultDialog(BuildContext context, UserWorkspaceState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
}
final actionType = actionResult.actionType;
final result = actionResult.result;
final isLoading = actionResult.isLoading;
if (isLoading) {
loadingIndicator ??= Loading(context)..start();
return;
} else {
loadingIndicator?.stop();
loadingIndicator = null;
}
if (result == null) {
return;
}
result.onFailure((f) {
Log.error(
'[Workspace] Failed to perform ${actionType.toString()} action: $f',
);
});
final String? message;
ToastificationType toastType = ToastificationType.success;
switch (actionType) {
case UserWorkspaceActionType.open:
message = result.fold(
(s) {
toastType = ToastificationType.success;
return LocaleKeys.workspace_openSuccess.tr();
},
(e) {
toastType = ToastificationType.error;
return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
},
);
break;
default:
message = null;
toastType = ToastificationType.error;
break;
}
if (message != null) {
showToastNotification(context, message: message, type: toastType);
}
}
}

View File

@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.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/cloud/cloud_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart'; import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
@ -79,8 +80,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
PersonalInfoSettingGroup( PersonalInfoSettingGroup(
userProfile: userProfile, userProfile: userProfile,
), ),
// TODO: Enable and implement along with Push Notifications const WorkspaceSettingGroup(),
// const NotificationsSettingGroup(),
const AppearanceSettingGroup(), const AppearanceSettingGroup(),
const LanguageSettingGroup(), const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(), if (Env.enableCustomCloud) const CloudSettingGroup(),

View File

@ -68,36 +68,34 @@ class _RecentViews extends StatelessWidget {
? const Color(0xFFE9E9EC) ? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF); : const Color(0x1AFFFFFF);
return SlidableAutoCloseBehavior( return SlidableAutoCloseBehavior(
child: Scrollbar( child: ListView.separated(
child: ListView.separated( key: const PageStorageKey('recent_views_page_storage_key'),
key: const PageStorageKey('recent_views_page_storage_key'), padding: EdgeInsets.only(
padding: EdgeInsets.only( bottom: HomeSpaceViewSizes.mVerticalPadding +
bottom: HomeSpaceViewSizes.mVerticalPadding + MediaQuery.of(context).padding.bottom,
MediaQuery.of(context).padding.bottom, ),
), itemBuilder: (context, index) {
itemBuilder: (context, index) { final sectionView = recentViews[index];
final sectionView = recentViews[index]; return Container(
return Container( padding: const EdgeInsets.symmetric(vertical: 24.0),
padding: const EdgeInsets.symmetric(vertical: 24.0), decoration: BoxDecoration(
decoration: BoxDecoration( border: Border(
border: Border( bottom: BorderSide(
bottom: BorderSide( color: borderColor,
color: borderColor, width: 0.5,
width: 0.5,
),
), ),
), ),
child: MobileViewPage( ),
key: ValueKey(sectionView.item.id), child: MobileViewPage(
view: sectionView.item, key: ValueKey(sectionView.item.id),
timestamp: sectionView.timestamp, view: sectionView.item,
type: MobilePageCardType.recent, timestamp: sectionView.timestamp,
), type: MobilePageCardType.recent,
); ),
}, );
separatorBuilder: (context, index) => const HSpace(8), },
itemCount: recentViews.length, separatorBuilder: (context, index) => const HSpace(8),
), itemCount: recentViews.length,
), ),
); );
} }

View File

@ -0,0 +1,81 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FloatingAIEntry extends StatelessWidget {
const FloatingAIEntry({super.key});
@override
Widget build(BuildContext context) {
return AnimatedGestureDetector(
scaleFactor: 0.99,
onTapUp: () => mobileCreateNewAIChatNotifier.value =
mobileCreateNewAIChatNotifier.value + 1,
child: DecoratedBox(
decoration: _buildShadowDecoration(context),
child: Container(
decoration: _buildWrapperDecoration(context),
height: 48,
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 18),
child: _buildHintText(context),
),
),
),
);
}
BoxDecoration _buildShadowDecoration(BuildContext context) {
return BoxDecoration(
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
blurRadius: 20,
spreadRadius: 1,
offset: const Offset(0, 4),
color: Colors.black.withOpacity(0.05),
),
],
);
}
BoxDecoration _buildWrapperDecoration(BuildContext context) {
final outlineColor = Theme.of(context).colorScheme.outline;
final borderColor = Theme.of(context).isLightMode
? outlineColor.withOpacity(0.7)
: outlineColor.withOpacity(0.3);
return BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.surface,
border: Border.fromBorderSide(
BorderSide(
color: borderColor,
),
),
);
}
Widget _buildHintText(BuildContext context) {
return Row(
children: [
FlowySvg(
FlowySvgs.toolbar_item_ai_s,
size: const Size.square(16.0),
color: Theme.of(context).hintColor,
opacity: 0.7,
),
const HSpace(8),
FlowyText(
LocaleKeys.chat_inputMessageHint.tr(),
color: Theme.of(context).hintColor,
),
],
);
}
}

View File

@ -6,9 +6,12 @@ import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dar
import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart'; import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -17,6 +20,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'ai_bubble_button.dart';
final ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);
class MobileSpaceTab extends StatefulWidget { class MobileSpaceTab extends StatefulWidget {
const MobileSpaceTab({ const MobileSpaceTab({
super.key, super.key,
@ -37,14 +44,19 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
void initState() { void initState() {
super.initState(); super.initState();
mobileCreateNewPageNotifier.addListener(_createNewPage); mobileCreateNewPageNotifier.addListener(_createNewDocument);
mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);
mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);
} }
@override @override
void dispose() { void dispose() {
tabController?.removeListener(_onTabChange); tabController?.removeListener(_onTabChange);
tabController?.dispose(); tabController?.dispose();
mobileCreateNewPageNotifier.removeListener(_createNewPage);
mobileCreateNewPageNotifier.removeListener(_createNewDocument);
mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);
mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);
super.dispose(); super.dispose();
} }
@ -140,7 +152,20 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
case MobileSpaceTabType.recent: case MobileSpaceTabType.recent:
return const MobileRecentSpace(); return const MobileRecentSpace();
case MobileSpaceTabType.spaces: case MobileSpaceTabType.spaces:
return MobileHomeSpace(userProfile: widget.userProfile); return Stack(
children: [
MobileHomeSpace(userProfile: widget.userProfile),
// only show ai chat button for cloud user
if (widget.userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud)
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16,
left: 20,
right: 20,
child: const FloatingAIEntry(),
),
],
);
case MobileSpaceTabType.favorites: case MobileSpaceTabType.favorites:
return MobileFavoriteSpace(userProfile: widget.userProfile); return MobileFavoriteSpace(userProfile: widget.userProfile);
default: default:
@ -150,15 +175,24 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
} }
// quick create new page when clicking the add button in navigation bar // quick create new page when clicking the add button in navigation bar
void _createNewPage() { void _createNewDocument() {
_createNewPage(ViewLayoutPB.Document);
}
void _createNewAIChat() {
_createNewPage(ViewLayoutPB.Chat);
}
void _createNewPage(ViewLayoutPB layout) {
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) { if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
context.read<SpaceBloc>().add( context.read<SpaceBloc>().add(
SpaceEvent.createPage( SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document, layout: layout,
), ),
); );
} else { } else if (layout == ViewLayoutPB.Document) {
// only support create document in section
context.read<SidebarSectionsBloc>().add( context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection( SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
@ -171,4 +205,16 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
); );
} }
} }
void _leaveWorkspace() {
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;
if (workspaceId == null) {
Log.error('Workspace ID is null');
return;
}
context
.read<UserWorkspaceBloc>()
.add(UserWorkspaceEvent.leaveWorkspace(workspaceId));
}
} }

View File

@ -7,7 +7,8 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/share_log_files.dart'; import 'package:appflowy/util/share_log_files.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -74,10 +75,14 @@ class SupportSettingGroup extends StatelessWidget {
actionButtonTitle: LocaleKeys.button_yes.tr(), actionButtonTitle: LocaleKeys.button_yes.tr(),
onActionButtonPressed: () async { onActionButtonPressed: () async {
await getIt<FlowyCacheManager>().clearAllCache(); await getIt<FlowyCacheManager>().clearAllCache();
// check the workspace and space health
await WorkspaceDataManager.checkViewHealth(
dryRun: false,
);
if (context.mounted) { if (context.mounted) {
showSnackBarMessage( showToastNotification(
context, context,
LocaleKeys.settings_files_clearCacheSuccess.tr(), message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
); );
} }
}, },

View File

@ -42,14 +42,24 @@ class UserSessionSettingGroup extends StatelessWidget {
MobileSignInOrLogoutButton( MobileSignInOrLogoutButton(
labelText: LocaleKeys.settings_menu_logout.tr(), labelText: LocaleKeys.settings_menu_logout.tr(),
onPressed: () async { onPressed: () async {
await showFlowyMobileConfirmDialog( await showFlowyCupertinoConfirmDialog(
context, title: LocaleKeys.settings_menu_logoutPrompt.tr(),
content: FlowyText( leftButton: FlowyText(
LocaleKeys.settings_menu_logoutPrompt.tr(), LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
), ),
actionButtonTitle: LocaleKeys.button_yes.tr(), rightButton: FlowyText(
actionButtonColor: Theme.of(context).colorScheme.error, LocaleKeys.button_logout.tr(),
onActionButtonPressed: () async { fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (context) async {
Navigator.of(context).pop();
await getIt<AuthService>().signOut(); await getIt<AuthService>().signOut();
await runAppFlowy(); await runAppFlowy();
}, },

View File

@ -0,0 +1,346 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:toastification/toastification.dart';
import 'member_list.dart';
ValueNotifier<int> mobileLeaveWorkspaceNotifier = ValueNotifier(0);
class InviteMembersScreen extends StatelessWidget {
const InviteMembersScreen({
super.key,
});
static const routeName = '/invite_member';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FlowyAppBar(
titleText: LocaleKeys.settings_appearance_members_label.tr(),
),
body: const _InviteMemberPage(),
);
}
}
class _InviteMemberPage extends StatefulWidget {
const _InviteMemberPage();
@override
State<_InviteMemberPage> createState() => _InviteMemberPageState();
}
class _InviteMemberPageState extends State<_InviteMemberPage> {
final emailController = TextEditingController();
late final Future<UserProfilePB?> userProfile;
bool exceededLimit = false;
@override
void initState() {
super.initState();
userProfile = UserBackendService.getCurrentUserProfile().fold(
(s) => s,
(f) => null,
);
}
@override
void dispose() {
emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: userProfile,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox.shrink();
}
if (snapshot.hasError || snapshot.data == null) {
return _buildError(context);
}
final userProfile = snapshot.data!;
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(const WorkspaceMemberEvent.initial()),
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _onListener,
builder: (context, state) {
return Column(
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (state.myRole.isOwner) ...[
Padding(
padding: const EdgeInsets.all(16.0),
child: _buildInviteMemberArea(context),
),
const VSpace(16),
],
if (state.members.isNotEmpty) ...[
const VSpace(8),
MobileMemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
],
),
),
if (state.myRole.isMember) const _LeaveWorkspaceButton(),
const VSpace(48),
],
);
},
),
);
},
);
}
Widget _buildInviteMemberArea(BuildContext context) {
return Column(
children: [
TextFormField(
autofocus: true,
controller: emailController,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),
),
),
const VSpace(16),
if (exceededLimit) ...[
FlowyText.regular(
LocaleKeys.settings_appearance_members_inviteFailedMemberLimit.tr(),
fontSize: 14.0,
maxLines: 3,
color: Theme.of(context).colorScheme.error,
),
const VSpace(16),
],
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _inviteMember(context),
child: Text(
LocaleKeys.settings_appearance_members_sendInvite.tr(),
),
),
),
],
);
}
Widget _buildError(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.medium(
LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),
fontSize: 18.0,
textAlign: TextAlign.center,
),
const VSpace(8.0),
FlowyText.regular(
LocaleKeys
.settings_appearance_members_workspaceMembersErrorDescription
.tr(),
fontSize: 17.0,
maxLines: 10,
textAlign: TextAlign.center,
lineHeight: 1.3,
color: Theme.of(context).hintColor,
),
],
),
),
);
}
void _onListener(BuildContext context, WorkspaceMemberState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
}
final actionType = actionResult.actionType;
final result = actionResult.result;
// only show the result dialog when the action is WorkspaceMemberActionType.add
if (actionType == WorkspaceMemberActionType.add) {
result.fold(
(s) {
showToastNotification(
context,
message:
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
);
},
(f) {
Log.error('add workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
context,
type: ToastificationType.error,
message: message,
);
},
);
} else if (actionType == WorkspaceMemberActionType.invite) {
result.fold(
(s) {
showToastNotification(
context,
message:
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
);
},
(f) {
Log.error('invite workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys.settings_appearance_members_inviteFailedMemberLimit
.tr()
: LocaleKeys.settings_appearance_members_failedToInviteMember
.tr();
setState(() {
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
});
showToastNotification(
context,
type: ToastificationType.error,
message: message,
);
},
);
} else if (actionType == WorkspaceMemberActionType.remove) {
result.fold(
(s) {
showToastNotification(
context,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceSuccess
.tr(),
);
},
(f) {
showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys
.settings_appearance_members_removeFromWorkspaceFailed
.tr(),
);
},
);
}
}
void _inviteMember(BuildContext context) {
final email = emailController.text;
if (!isEmail(email)) {
return showToastNotification(
context,
type: ToastificationType.error,
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
}
context
.read<WorkspaceMemberBloc>()
.add(WorkspaceMemberEvent.inviteWorkspaceMember(email));
// clear the email field after inviting
emailController.clear();
}
}
class _LeaveWorkspaceButton extends StatelessWidget {
const _LeaveWorkspaceButton();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.error,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(
color: Theme.of(context).colorScheme.error,
width: 0.5,
),
),
),
onPressed: () => _leaveWorkspace(context),
child: FlowyText(
LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
fontSize: 14.0,
color: Theme.of(context).colorScheme.error,
fontWeight: FontWeight.w500,
),
),
);
}
void _leaveWorkspace(BuildContext context) {
showFlowyCupertinoConfirmDialog(
title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
LocaleKeys.button_confirm.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (buttonContext) async {
// try to use popUntil with a specific route name but failed
// so use pop twice as a workaround
Navigator.of(buttonContext).pop();
Navigator.of(context).pop();
Navigator.of(context).pop();
mobileLeaveWorkspaceNotifier.value =
mobileLeaveWorkspaceNotifier.value + 1;
},
);
}
}

View File

@ -0,0 +1,164 @@
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';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.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/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class MobileMemberList extends StatelessWidget {
const MobileMemberList({
super.key,
required this.members,
required this.myRole,
required this.userProfile,
});
final List<WorkspaceMemberPB> members;
final AFRolePB myRole;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return SlidableAutoCloseBehavior(
child: SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const FlowyDivider(
padding: EdgeInsets.symmetric(horizontal: 16.0),
),
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: FlowyText.semibold(
LocaleKeys.settings_appearance_members_label.tr(),
fontSize: 16.0,
),
),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
),
);
}
}
class _MemberItem extends StatelessWidget {
const _MemberItem({
required this.member,
required this.myRole,
required this.userProfile,
});
final WorkspaceMemberPB member;
final AFRolePB myRole;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final canDelete = myRole.canDelete && member.email != userProfile.email;
final textColor = member.role.isOwner ? Theme.of(context).hintColor : null;
Widget child = Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: FlowyText.medium(
member.name,
color: textColor,
fontSize: 15.0,
),
),
Expanded(
child: FlowyText.medium(
member.role.description,
color: textColor,
fontSize: 15.0,
textAlign: TextAlign.end,
),
),
],
),
);
if (canDelete) {
child = Slidable(
key: ValueKey(member.email),
endActionPane: ActionPane(
extentRatio: 1 / 6.0,
motion: const ScrollMotion(),
children: [
CustomSlidableAction(
backgroundColor: const Color(0xE5515563),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
onPressed: (context) {
HapticFeedback.mediumImpact();
_showDeleteMenu(context);
},
padding: EdgeInsets.zero,
child: const FlowySvg(
FlowySvgs.three_dots_s,
size: Size.square(24),
color: Colors.white,
),
),
],
),
child: child,
);
}
return child;
}
void _showDeleteMenu(BuildContext context) {
final workspaceMemberBloc = context.read<WorkspaceMemberBloc>();
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) {
return FlowyOptionTile.text(
text: LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(),
height: 52.0,
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.trash_s,
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
showBottomBorder: false,
onTap: () {
workspaceMemberBloc.add(
WorkspaceMemberEvent.removeWorkspaceMember(
member.email,
),
);
Navigator.of(context).pop();
},
);
},
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../widgets/widgets.dart';
import 'invite_members_screen.dart';
class WorkspaceSettingGroup extends StatelessWidget {
const WorkspaceSettingGroup({
super.key,
});
@override
Widget build(BuildContext context) {
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_appearance_members_label.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_appearance_members_label.tr(),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.push(InviteMembersScreen.routeName);
},
),
],
);
}
}

View File

@ -98,12 +98,13 @@ Future<T?> showFlowyCupertinoConfirmDialog<T>({
}) { }) {
return showDialog( return showDialog(
context: context ?? AppGlobals.context, context: context ?? AppGlobals.context,
barrierColor: Colors.black.withOpacity(0.25),
builder: (context) => CupertinoAlertDialog( builder: (context) => CupertinoAlertDialog(
title: FlowyText.medium( title: FlowyText.medium(
title, title,
fontSize: 18, fontSize: 16,
maxLines: 10, maxLines: 10,
lineHeight: 1.3, figmaLineHeight: 22.0,
), ),
actions: [ actions: [
CupertinoDialogAction( CupertinoDialogAction(

View File

@ -425,13 +425,14 @@ class _ChatContentPageState extends State<_ChatContentPage> {
}, },
), ),
const VSpace(6), const VSpace(6),
Opacity( if (PlatformExtension.isDesktop)
opacity: 0.6, Opacity(
child: FlowyText( opacity: 0.6,
LocaleKeys.chat_aiMistakePrompt.tr(), child: FlowyText(
fontSize: 12, LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
),
), ),
),
], ],
); );
}, },

View File

@ -73,6 +73,7 @@ class ChatWelcomePage extends StatelessWidget {
const VSpace(8), const VSpace(8),
Wrap( Wrap(
direction: Axis.vertical, direction: Axis.vertical,
spacing: isMobile ? 12.0 : 0.0,
children: items children: items
.map( .map(
(i) => WelcomeQuestionWidget( (i) => WelcomeQuestionWidget(

View File

@ -23,9 +23,9 @@ class RelatedRowDetailPageBloc
@override @override
Future<void> close() { Future<void> close() {
state.whenOrNull( state.whenOrNull(
ready: (databaseController, rowController) { ready: (databaseController, rowController) async {
rowController.dispose(); await rowController.dispose();
databaseController.dispose(); await databaseController.dispose();
}, },
); );
return super.close(); return super.close();
@ -36,8 +36,8 @@ class RelatedRowDetailPageBloc
event.when( event.when(
didInitialize: (databaseController, rowController) { didInitialize: (databaseController, rowController) {
state.maybeWhen( state.maybeWhen(
ready: (_, oldRowController) { ready: (_, oldRowController) async {
oldRowController.dispose(); await oldRowController.dispose();
emit( emit(
RelatedRowDetailPageState.ready( RelatedRowDetailPageState.ready(
databaseController: databaseController, databaseController: databaseController,

View File

@ -81,6 +81,12 @@ class RowCache {
_changedNotifier.receive(const ChangedReason.setInitialRows()); _changedNotifier.receive(const ChangedReason.setInitialRows());
} }
void setRowMeta(RowMetaPB rowMeta) {
final rowInfo = buildGridRow(rowMeta);
_rowList.add(rowInfo);
_changedNotifier.receive(const ChangedReason.didFetchRow());
}
void dispose() { void dispose() {
_rowLifeCycle.onRowDisposed(); _rowLifeCycle.onRowDisposed();
_changedNotifier.dispose(); _changedNotifier.dispose();
@ -215,7 +221,8 @@ class RowCache {
if (rowInfo == null) { if (rowInfo == null) {
_loadRow(rowMeta.id); _loadRow(rowMeta.id);
} }
return _makeCells(rowMeta); final cells = _makeCells(rowMeta);
return cells;
} }
Future<void> _loadRow(RowId rowId) async { Future<void> _loadRow(RowId rowId) async {
@ -277,6 +284,7 @@ class RowChangesetNotifier extends ChangeNotifier {
reorderRows: (_) => notifyListeners(), reorderRows: (_) => notifyListeners(),
reorderSingleRow: (_) => notifyListeners(), reorderSingleRow: (_) => notifyListeners(),
setInitialRows: (_) => notifyListeners(), setInitialRows: (_) => notifyListeners(),
didFetchRow: (_) => notifyListeners(),
); );
} }
} }
@ -305,6 +313,7 @@ class ChangedReason with _$ChangedReason {
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update; const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory ChangedReason.fieldDidChange() = _FieldDidChange; const factory ChangedReason.fieldDidChange() = _FieldDidChange;
const factory ChangedReason.initial() = InitialListState; const factory ChangedReason.initial() = InitialListState;
const factory ChangedReason.didFetchRow() = _DidFetchRow;
const factory ChangedReason.reorderRows() = _ReorderRows; const factory ChangedReason.reorderRows() = _ReorderRows;
const factory ChangedReason.reorderSingleRow( const factory ChangedReason.reorderSingleRow(
ReorderSingleRowPB reorderRow, ReorderSingleRowPB reorderRow,

View File

@ -1,3 +1,5 @@
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/row_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,35 +11,60 @@ typedef OnRowChanged = void Function(List<CellContext>, ChangedReason);
class RowController { class RowController {
RowController({ RowController({
required this.rowMeta, required RowMetaPB rowMeta,
required this.viewId, required this.viewId,
required RowCache rowCache, required RowCache rowCache,
this.groupId, this.groupId,
}) : _rowCache = rowCache; }) : _rowMeta = rowMeta,
_rowCache = rowCache,
_rowBackendSvc = RowBackendService(viewId: viewId),
_rowListener = RowListener(rowMeta.id) {
_rowBackendSvc.initRow(rowMeta.id);
_rowListener.start(
onMetaChanged: (newRowMeta) {
if (_isDisposed) {
return;
}
_rowMeta = newRowMeta;
_rowCache.setRowMeta(newRowMeta);
},
);
}
final RowMetaPB rowMeta; RowMetaPB _rowMeta;
final String? groupId; final String? groupId;
final String viewId; final String viewId;
final List<VoidCallback> _onRowChangedListeners = []; final List<VoidCallback> _onRowChangedListeners = [];
final RowCache _rowCache; final RowCache _rowCache;
final RowListener _rowListener;
final RowBackendService _rowBackendSvc;
bool _isDisposed = false;
CellMemCache get cellCache => _rowCache.cellCache; CellMemCache get cellCache => _rowCache.cellCache;
String get rowId => rowMeta.id; String get rowId => rowMeta.id;
RowMetaPB get rowMeta => _rowMeta;
List<CellContext> loadData() => _rowCache.loadCells(rowMeta); List<CellContext> loadCells() => _rowCache.loadCells(rowMeta);
void addListener({OnRowChanged? onRowChanged}) { void addListener({OnRowChanged? onRowChanged}) {
final fn = _rowCache.addListener( final fn = _rowCache.addListener(
rowId: rowMeta.id, rowId: rowMeta.id,
onRowChanged: onRowChanged, onRowChanged: (context, reasons) {
if (_isDisposed) {
return;
}
onRowChanged?.call(context, reasons);
},
); );
// Add the listener to the list so that we can remove it later. // Add the listener to the list so that we can remove it later.
_onRowChangedListeners.add(fn); _onRowChangedListeners.add(fn);
} }
void dispose() { Future<void> dispose() async {
_isDisposed = true;
await _rowListener.stop();
for (final fn in _onRowChangedListeners) { for (final fn in _onRowChangedListeners) {
_rowCache.removeRowListener(fn); _rowCache.removeRowListener(fn);
} }

View File

@ -37,6 +37,14 @@ class RowBackendService {
return DatabaseEventCreateRow(payload).send(); return DatabaseEventCreateRow(payload).send();
} }
Future<FlowyResult<void, FlowyError>> initRow(RowId rowId) async {
final payload = RowIdPB()
..viewId = viewId
..rowId = rowId;
return DatabaseEventInitRow(payload).send();
}
Future<FlowyResult<RowMetaPB, FlowyError>> createRowBefore(RowId rowId) { Future<FlowyResult<RowMetaPB, FlowyError>> createRowBefore(RowId rowId) {
return createRow( return createRow(
viewId: viewId, viewId: viewId,

View File

@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import '../defines.dart'; import '../defines.dart';
import '../field/field_controller.dart'; import '../field/field_controller.dart';
@ -91,6 +93,17 @@ class DatabaseViewCache {
(reorderRow) => _rowCache.reorderSingleRow(reorderRow), (reorderRow) => _rowCache.reorderSingleRow(reorderRow),
(err) => Log.error(err), (err) => Log.error(err),
), ),
onReloadRows: () {
final payload = DatabaseViewIdPB(value: viewId);
DatabaseEventGetAllRows(payload).send().then((result) {
result.fold(
(rows) {
_rowCache.setInitialRows(rows.items);
},
(err) => Log.error(err),
);
});
},
); );
_rowCache.onRowsChanged( _rowCache.onRowsChanged(

View File

@ -7,85 +7,96 @@ import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'
import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef RowsVisibilityNotifierValue typedef RowsVisibilityCallback = void Function(
= FlowyResult<RowsVisibilityChangePB, FlowyError>; FlowyResult<RowsVisibilityChangePB, FlowyError>,
);
typedef NumberOfRowsNotifierValue = FlowyResult<RowsChangePB, FlowyError>; typedef NumberOfRowsCallback = void Function(
typedef ReorderAllRowsNotifierValue = FlowyResult<List<String>, FlowyError>; FlowyResult<RowsChangePB, FlowyError>,
typedef SingleRowNotifierValue = FlowyResult<ReorderSingleRowPB, FlowyError>; );
typedef ReorderAllRowsCallback = void Function(
FlowyResult<List<String>, FlowyError>,
);
typedef SingleRowCallback = void Function(
FlowyResult<ReorderSingleRowPB, FlowyError>,
);
class DatabaseViewListener { class DatabaseViewListener {
DatabaseViewListener({required this.viewId}); DatabaseViewListener({required this.viewId});
final String viewId; final String viewId;
PublishNotifier<NumberOfRowsNotifierValue>? _rowsNotifier = PublishNotifier();
PublishNotifier<ReorderAllRowsNotifierValue>? _reorderAllRows =
PublishNotifier();
PublishNotifier<SingleRowNotifierValue>? _reorderSingleRow =
PublishNotifier();
PublishNotifier<RowsVisibilityNotifierValue>? _rowsVisibility =
PublishNotifier();
DatabaseNotificationListener? _listener; DatabaseNotificationListener? _listener;
void start({ void start({
required void Function(NumberOfRowsNotifierValue) onRowsChanged, required NumberOfRowsCallback onRowsChanged,
required void Function(ReorderAllRowsNotifierValue) onReorderAllRows, required ReorderAllRowsCallback onReorderAllRows,
required void Function(SingleRowNotifierValue) onReorderSingleRow, required SingleRowCallback onReorderSingleRow,
required void Function(RowsVisibilityNotifierValue) onRowsVisibilityChanged, required RowsVisibilityCallback onRowsVisibilityChanged,
required void Function() onReloadRows,
}) { }) {
if (_listener != null) { // Stop any existing listener
_listener?.stop(); _listener?.stop();
}
// Initialize the notification listener
_listener = DatabaseNotificationListener( _listener = DatabaseNotificationListener(
objectId: viewId, objectId: viewId,
handler: _handler, handler: (ty, result) => _handler(
ty,
result,
onRowsChanged,
onReorderAllRows,
onReorderSingleRow,
onRowsVisibilityChanged,
onReloadRows,
),
); );
_rowsNotifier?.addPublishListener(onRowsChanged);
_rowsVisibility?.addPublishListener(onRowsVisibilityChanged);
_reorderAllRows?.addPublishListener(onReorderAllRows);
_reorderSingleRow?.addPublishListener(onReorderSingleRow);
} }
void _handler( void _handler(
DatabaseNotification ty, DatabaseNotification ty,
FlowyResult<Uint8List, FlowyError> result, FlowyResult<Uint8List, FlowyError> result,
NumberOfRowsCallback onRowsChanged,
ReorderAllRowsCallback onReorderAllRows,
SingleRowCallback onReorderSingleRow,
RowsVisibilityCallback onRowsVisibilityChanged,
void Function() onReloadRows,
) { ) {
switch (ty) { switch (ty) {
case DatabaseNotification.DidUpdateViewRowsVisibility: case DatabaseNotification.DidUpdateViewRowsVisibility:
result.fold( result.fold(
(payload) => _rowsVisibility?.value = (payload) => onRowsVisibilityChanged(
FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)), FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)),
(error) => _rowsVisibility?.value = FlowyResult.failure(error), ),
(error) => onRowsVisibilityChanged(FlowyResult.failure(error)),
); );
break; break;
case DatabaseNotification.DidUpdateRow: case DatabaseNotification.DidUpdateRow:
result.fold( result.fold(
(payload) => _rowsNotifier?.value = (payload) => onRowsChanged(
FlowyResult.success(RowsChangePB.fromBuffer(payload)), FlowyResult.success(RowsChangePB.fromBuffer(payload)),
(error) => _rowsNotifier?.value = FlowyResult.failure(error), ),
(error) => onRowsChanged(FlowyResult.failure(error)),
); );
break; break;
case DatabaseNotification.DidReorderRows: case DatabaseNotification.DidReorderRows:
result.fold( result.fold(
(payload) => _reorderAllRows?.value = FlowyResult.success( (payload) => onReorderAllRows(
ReorderAllRowsPB.fromBuffer(payload).rowOrders, FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders),
), ),
(error) => _reorderAllRows?.value = FlowyResult.failure(error), (error) => onReorderAllRows(FlowyResult.failure(error)),
); );
break; break;
case DatabaseNotification.DidReorderSingleRow: case DatabaseNotification.DidReorderSingleRow:
result.fold( result.fold(
(payload) => _reorderSingleRow?.value = (payload) => onReorderSingleRow(
FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)),
(error) => _reorderSingleRow?.value = FlowyResult.failure(error), ),
(error) => onReorderSingleRow(FlowyResult.failure(error)),
); );
break; break;
case DatabaseNotification.ReloadRows:
onReloadRows();
break;
default: default:
break; break;
} }
@ -93,16 +104,6 @@ class DatabaseViewListener {
Future<void> stop() async { Future<void> stop() async {
await _listener?.stop(); await _listener?.stop();
_rowsVisibility?.dispose(); _listener = null;
_rowsVisibility = null;
_rowsNotifier?.dispose();
_rowsNotifier = null;
_reorderAllRows?.dispose();
_reorderAllRows = null;
_reorderSingleRow?.dispose();
_reorderSingleRow = null;
} }
} }

View File

@ -34,7 +34,7 @@ class CalendarEventEditorBloc
.firstWhere((fieldInfo) => fieldInfo.isPrimary) .firstWhere((fieldInfo) => fieldInfo.isPrimary)
.id; .id;
final cells = rowController final cells = rowController
.loadData() .loadCells()
.where( .where(
(cellContext) => (cellContext) =>
_filterCellContext(cellContext, primaryFieldId), _filterCellContext(cellContext, primaryFieldId),
@ -88,7 +88,7 @@ class CalendarEventEditorBloc
@override @override
Future<void> close() async { Future<void> close() async {
rowController.dispose(); await rowController.dispose();
return super.close(); return super.close();
} }
} }

View File

@ -39,11 +39,13 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
_startListening(); _startListening();
await _getAllCalculations(); await _getAllCalculations();
add( if (!isClosed) {
CalculationsEvent.didReceiveFieldUpdate( add(
_fieldController.fieldInfos, CalculationsEvent.didReceiveFieldUpdate(
), _fieldController.fieldInfos,
); ),
);
}
}, },
didReceiveFieldUpdate: (fields) async { didReceiveFieldUpdate: (fields) async {
emit( emit(
@ -131,6 +133,10 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
Future<void> _getAllCalculations() async { Future<void> _getAllCalculations() async {
final calculationsOrFailure = await _calculationsService.getCalculations(); final calculationsOrFailure = await _calculationsService.getCalculations();
if (isClosed) {
return;
}
final RepeatedCalculationsPB? calculations = final RepeatedCalculationsPB? calculations =
calculationsOrFailure.fold((s) => s, (e) => null); calculationsOrFailure.fold((s) => s, (e) => null);
if (calculations != null) { if (calculations != null) {

View File

@ -36,7 +36,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
@override @override
Future<void> close() async { Future<void> close() async {
_rowController.dispose(); await _rowController.dispose();
return super.close(); return super.close();
} }
@ -82,7 +82,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
void _init() { void _init() {
add( add(
RowEvent.didReceiveCells( RowEvent.didReceiveCells(
_rowController.loadData(), _rowController.loadCells(),
const ChangedReason.setInitialRows(), const ChangedReason.setInitialRows(),
), ),
); );

View File

@ -29,7 +29,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@override @override
Future<void> close() async { Future<void> close() async {
rowController.dispose(); await rowController.dispose();
return super.close(); return super.close();
} }
@ -125,7 +125,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
} }
void _init() { void _init() {
allCells.addAll(rowController.loadData()); allCells.addAll(rowController.loadCells());
int numHiddenFields = 0; int numHiddenFields = 0;
final visibleCells = <CellContext>[]; final visibleCells = <CellContext>[];
for (final cell in allCells) { for (final cell in allCells) {

View File

@ -9,7 +9,6 @@ import 'package:appflowy/workspace/application/action_navigation/navigation_acti
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
@ -154,6 +153,7 @@ class _GridPageState extends State<GridPage> {
finish: (result) => result.successOrFail.fold( finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts( (_) => GridShortcuts(
child: GridPageContent( child: GridPageContent(
key: ValueKey(widget.view.id),
view: widget.view, view: widget.view,
), ),
), ),
@ -331,33 +331,10 @@ class _GridRowsState extends State<_GridRows> {
BuildContext context, BuildContext context,
GridState state, GridState state,
) { ) {
final children = state.rowInfos.mapIndexed((index, rowInfo) { // 1. GridRowBottomBar
return _renderRow( // 2. GridCalculationsRow
context, // 3. Footer Padding
rowInfo.rowId, final itemCount = state.rowInfos.length + 3;
isDraggable: state.reorderable,
index: index,
);
}).toList()
..add(const GridRowBottomBar(key: Key('grid_footer')));
if (showFloatingCalculations) {
children.add(
const SizedBox(
key: Key('calculations_bottom_padding'),
height: 36,
),
);
} else {
children.add(
GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
),
);
}
children.add(const SizedBox(key: Key('footer_padding'), height: 10));
return Stack( return Stack(
children: [ children: [
@ -381,8 +358,37 @@ class _GridRowsState extends State<_GridRows> {
.add(GridEvent.moveRow(fromIndex, toIndex)); .add(GridEvent.moveRow(fromIndex, toIndex));
} }
}, },
itemCount: children.length, itemCount: itemCount,
itemBuilder: (context, index) => children[index], itemBuilder: (context, index) {
if (index < state.rowInfos.length) {
return _renderRow(
context,
state.rowInfos[index].rowId,
isDraggable: state.reorderable,
index: index,
);
}
if (index == state.rowInfos.length) {
return const GridRowBottomBar(key: Key('grid_footer'));
}
if (index == state.rowInfos.length + 1) {
if (showFloatingCalculations) {
return const SizedBox(
key: Key('calculations_bottom_padding'),
height: 36,
);
} else {
return GridCalculationsRow(
key: const Key('grid_calculations'),
viewId: widget.viewId,
);
}
}
return const SizedBox(key: Key('footer_padding'), height: 10);
},
), ),
), ),
if (showFloatingCalculations) ...[ if (showFloatingCalculations) ...[

View File

@ -6,6 +6,7 @@ class GridSize {
static double get scrollBarSize => 8 * scale; static double get scrollBarSize => 8 * scale;
static double get headerHeight => 40 * scale; static double get headerHeight => 40 * scale;
static double get buttonHeight => 38 * scale;
static double get footerHeight => 40 * scale; static double get footerHeight => 40 * scale;
static double get horizontalHeaderPadding => static double get horizontalHeaderPadding =>
PlatformExtension.isDesktop ? 40 * scale : 16 * scale; PlatformExtension.isDesktop ? 40 * scale : 16 * scale;

View File

@ -81,7 +81,7 @@ class _MobileGridRowState extends State<MobileGridRow> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
_rowController.dispose(); await _rowController.dispose();
super.dispose(); super.dispose();
} }
} }

View File

@ -188,8 +188,14 @@ class _RowMenuButtonState extends State<RowMenuButton> {
richTooltipText: widget.isDragEnabled richTooltipText: widget.isDragEnabled
? TextSpan( ? TextSpan(
children: [ children: [
TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'), TextSpan(
TextSpan(text: LocaleKeys.tooltip_openMenu.tr()), text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.tooltip_openMenu.tr(),
style: context.tooltipTextStyle(),
),
], ],
) )
: null, : null,

View File

@ -57,7 +57,7 @@ class _DatabaseViewSettingContent extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: GridSize.horizontalHeaderPadding, horizontal: GridSize.horizontalHeaderPadding + 40,
), ),
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -13,42 +13,54 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
FocusNode focusNode, FocusNode focusNode,
TextEditingController textEditingController, TextEditingController textEditingController,
) { ) {
return Column( return Container(
children: [ decoration: BoxDecoration(
TextField( border: Border.fromBorderSide(
controller: textEditingController, BorderSide(color: Theme.of(context).colorScheme.outline),
readOnly: true,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
), ),
Row( borderRadius: const BorderRadius.all(Radius.circular(14)),
children: [ ),
const Spacer(), padding: const EdgeInsets.symmetric(
Padding( horizontal: 4,
padding: const EdgeInsets.all(8.0), vertical: 2,
child: SummaryCellAccessory( ),
viewId: bloc.cellController.viewId, child: Column(
fieldId: bloc.cellController.fieldId, children: [
rowId: bloc.cellController.rowId, TextField(
), controller: textEditingController,
readOnly: true,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
), ),
], ),
), Row(
], children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: SummaryCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
),
); );
} }
} }

View File

@ -13,42 +13,54 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
FocusNode focusNode, FocusNode focusNode,
TextEditingController textEditingController, TextEditingController textEditingController,
) { ) {
return Column( return Container(
children: [ decoration: BoxDecoration(
TextField( border: Border.fromBorderSide(
readOnly: true, BorderSide(color: Theme.of(context).colorScheme.outline),
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
), ),
Row( borderRadius: const BorderRadius.all(Radius.circular(14)),
children: [ ),
const Spacer(), padding: const EdgeInsets.symmetric(
Padding( horizontal: 4,
padding: const EdgeInsets.all(8.0), vertical: 2,
child: TranslateCellAccessory( ),
viewId: bloc.cellController.viewId, child: Column(
fieldId: bloc.cellController.fieldId, children: [
rowId: bloc.cellController.rowId, TextField(
), readOnly: true,
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
maxLines: null,
minLines: 1,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
), ),
], ),
), Row(
], children: [
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TranslateCellAccessory(
viewId: bloc.cellController.viewId,
fieldId: bloc.cellController.fieldId,
rowId: bloc.cellController.rowId,
),
),
],
),
],
),
); );
} }
} }

View File

@ -1,8 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@ -21,10 +18,11 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter_bloc/flutter_bloc.dart';
import '../cell/editable_cell_builder.dart'; import '../cell/editable_cell_builder.dart';
import 'accessory/cell_accessory.dart'; import 'accessory/cell_accessory.dart';
/// Display the row properties in a list. Only used in [RowDetailPage]. /// Display the row properties in a list. Only used in [RowDetailPage].
@ -165,6 +163,7 @@ class _PropertyCellState extends State<_PropertyCell> {
svg: FlowySvgs.drag_element_s, svg: FlowySvgs.drag_element_s,
richMessage: TextSpan( richMessage: TextSpan(
text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(),
style: context.tooltipTextStyle(),
), ),
), ),
), ),

View File

@ -1,15 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -47,20 +44,16 @@ class ViewTitleBarWithRow extends StatelessWidget {
if (state.ancestors.isEmpty) { if (state.ancestors.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
const maxWidth = WindowSizeManager.minWindowWidth - 200; return SingleChildScrollView(
return LayoutBuilder( scrollDirection: Axis.horizontal,
builder: (context, constraints) { child: SizedBox(
return Visibility( height: 24,
visible: maxWidth < constraints.maxWidth, child: Row(
// if the width is too small, only show one view title bar without the ancestors // refresh the view title bar when the ancestors changed
replacement: _buildRowName(), key: ValueKey(state.ancestors.hashCode),
child: Row( children: _buildViewTitles(state.ancestors),
// refresh the view title bar when the ancestors changed ),
key: ValueKey(state.ancestors.hashCode), ),
children: _buildViewTitles(state.ancestors),
),
);
},
); );
}, },
), ),
@ -71,16 +64,22 @@ class ViewTitleBarWithRow extends StatelessWidget {
// if the level is too deep, only show the root view, the database view and the row // if the level is too deep, only show the root view, the database view and the row
return views.length > 2 return views.length > 2
? [ ? [
_buildViewButton(views.first), _buildViewButton(views[1]),
const FlowyText.regular('/'), const FlowySvg(FlowySvgs.title_bar_divider_s),
const FlowyText.regular(' ... /'), const FlowyText.regular(' ... '),
const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildViewButton(views.last), _buildViewButton(views.last),
const FlowyText.regular('/'), const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildRowName(), _buildRowName(),
] ]
: [ : [
...views ...views
.map((e) => [_buildViewButton(e), const FlowyText.regular('/')]) .map(
(e) => [
_buildViewButton(e),
const FlowySvg(FlowySvgs.title_bar_divider_s),
],
)
.flattened, .flattened,
_buildRowName(), _buildRowName(),
]; ];
@ -89,9 +88,9 @@ class ViewTitleBarWithRow extends StatelessWidget {
Widget _buildViewButton(ViewPB view) { Widget _buildViewButton(ViewPB view) {
return FlowyTooltip( return FlowyTooltip(
message: view.name, message: view.name,
child: _ViewTitle( child: ViewTitle(
view: view, view: view,
behavior: _ViewTitleBehavior.uneditable, behavior: ViewTitleBehavior.uneditable,
onUpdated: () {}, onUpdated: () {},
), ),
); );
@ -180,11 +179,14 @@ class _TitleSkin extends IEditableTextCellSkin {
onTap: () {}, onTap: () {},
text: Row( text: Row(
children: [ children: [
EmojiText( if (state.icon != null) ...[
emoji: state.icon ?? "", FlowyText.emoji(
fontSize: 18.0, state.icon!,
), fontSize: 14.0,
const HSpace(2.0), figmaLineHeight: 18.0,
),
const HSpace(4.0),
],
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180), constraints: const BoxConstraints(maxWidth: 180),
child: FlowyText.regular( child: FlowyText.regular(
@ -204,106 +206,6 @@ class _TitleSkin extends IEditableTextCellSkin {
} }
} }
enum _ViewTitleBehavior {
editable,
uneditable,
}
class _ViewTitle extends StatefulWidget {
const _ViewTitle({
required this.view,
this.behavior = _ViewTitleBehavior.editable,
required this.onUpdated,
}) : maxTitleWidth = 180;
final ViewPB view;
final _ViewTitleBehavior behavior;
final double maxTitleWidth;
final VoidCallback onUpdated;
@override
State<_ViewTitle> createState() => _ViewTitleState();
}
class _ViewTitleState extends State<_ViewTitle> {
late final viewListener = ViewListener(viewId: widget.view.id);
String name = '';
String icon = '';
@override
void initState() {
super.initState();
name = widget.view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: widget.view.name;
icon = widget.view.icon.value;
viewListener.start(
onViewUpdated: (view) {
if (name != view.name || icon != view.icon.value) {
widget.onUpdated();
}
setState(() {
name = view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: view.name;
icon = view.icon.value;
});
},
);
}
@override
void dispose() {
viewListener.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
// root view
if (widget.view.parentViewId.isEmpty) {
return Row(
children: [
FlowyText.regular(name),
const HSpace(4.0),
],
);
}
final child = Row(
children: [
EmojiText(
emoji: icon,
fontSize: 18.0,
),
const HSpace(2.0),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxTitleWidth,
),
child: FlowyText.regular(
name,
overflow: TextOverflow.ellipsis,
),
),
],
);
return Listener(
onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),
child: FlowyButton(
useIntrinsicWidth: true,
onTap: () {},
text: child,
),
);
}
}
class RenameRowPopover extends StatefulWidget { class RenameRowPopover extends StatefulWidget {
const RenameRowPopover({ const RenameRowPopover({
super.key, super.key,

View File

@ -39,6 +39,10 @@ class DocumentCollaboratorsBloc
if (userProfile != null) { if (userProfile != null) {
_listener.start( _listener.start(
onDocAwarenessUpdate: (states) { onDocAwarenessUpdate: (states) {
if (isClosed) {
return;
}
add( add(
DocumentCollaboratorsEvent.update( DocumentCollaboratorsEvent.update(
userProfile, userProfile,

View File

@ -2,6 +2,7 @@ library document_plugin;
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
@ -118,6 +119,8 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
} }
}); });
final fixedTitle = data?[MobileDocumentScreen.viewFixedTitle];
return BlocProvider<ViewInfoBloc>.value( return BlocProvider<ViewInfoBloc>.value(
value: bloc, value: bloc,
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>( child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
@ -126,6 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
view: view, view: view,
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex), onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
initialSelection: initialSelection, initialSelection: initialSelection,
fixedTitle: fixedTitle,
), ),
), ),
); );

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.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/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
@ -26,6 +24,7 @@ import 'package:cross_file/cross_file.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -42,11 +41,13 @@ class DocumentPage extends StatefulWidget {
required this.view, required this.view,
required this.onDeleted, required this.onDeleted,
this.initialSelection, this.initialSelection,
this.fixedTitle,
}); });
final ViewPB view; final ViewPB view;
final VoidCallback onDeleted; final VoidCallback onDeleted;
final Selection? initialSelection; final Selection? initialSelection;
final String? fixedTitle;
@override @override
State<DocumentPage> createState() => _DocumentPageState(); State<DocumentPage> createState() => _DocumentPageState();
@ -103,6 +104,7 @@ class _DocumentPageState extends State<DocumentPage>
BlocProvider.value(value: documentBloc), BlocProvider.value(value: documentBloc),
], ],
child: BlocBuilder<DocumentBloc, DocumentState>( child: BlocBuilder<DocumentBloc, DocumentState>(
buildWhen: _shouldRebuildDocument,
builder: (context, state) { builder: (context, state) {
if (state.isLoading) { if (state.isLoading) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());
@ -195,13 +197,15 @@ class _DocumentPageState extends State<DocumentPage>
final isLocalMode = context.read<DocumentBloc>().isLocalMode; final isLocalMode = context.read<DocumentBloc>().isLocalMode;
final List<XFile> imageFiles = []; final List<XFile> imageFiles = [];
final List<XFile> otherfiles = []; final List<XFile> otherFiles = [];
for (final file in details.files) { for (final file in details.files) {
final fileName = file.name.toLowerCase();
if (file.mimeType?.startsWith('image/') ?? if (file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name)) { false || imgExtensionRegex.hasMatch(fileName)) {
imageFiles.add(file); imageFiles.add(file);
} else { } else {
otherfiles.add(file); otherFiles.add(file);
} }
} }
@ -213,7 +217,7 @@ class _DocumentPageState extends State<DocumentPage>
); );
await editorState!.dropFiles( await editorState!.dropFiles(
data.dropTarget!, data.dropTarget!,
otherfiles, otherFiles,
widget.view.id, widget.view.id,
isLocalMode, isLocalMode,
); );
@ -261,6 +265,7 @@ class _DocumentPageState extends State<DocumentPage>
if (PlatformExtension.isMobile) { if (PlatformExtension.isMobile) {
return DocumentImmersiveCover( return DocumentImmersiveCover(
fixedTitle: widget.fixedTitle,
view: widget.view, view: widget.view,
userProfilePB: userProfilePB, userProfilePB: userProfilePB,
); );
@ -308,4 +313,31 @@ class _DocumentPageState extends State<DocumentPage>
} }
} }
} }
bool _shouldRebuildDocument(DocumentState previous, DocumentState current) {
// only rebuild the document page when the below fields are changed
// this is to prevent unnecessary rebuilds
//
// If you confirm the newly added fields should be rebuilt, please update
// this function.
if (previous.editorState != current.editorState) {
return true;
}
if (previous.forceClose != current.forceClose ||
previous.isDeleted != current.isDeleted) {
return true;
}
if (previous.userProfilePB != current.userProfilePB) {
return true;
}
if (previous.isLoading != current.isLoading ||
previous.error != current.error) {
return true;
}
return false;
}
} }

View File

@ -419,7 +419,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
imageSlashMenuItem, imageSlashMenuItem,
bulletedListSlashMenuItem, bulletedListSlashMenuItem,
numberedListSlashMenuItem, numberedListSlashMenuItem,
todoListSlashMenuItem,
dividerSlashMenuItem,
quoteSlashMenuItem, quoteSlashMenuItem,
tableSlashMenuItem,
referencedDocSlashMenuItem, referencedDocSlashMenuItem,
gridSlashMenuItem(documentBloc), gridSlashMenuItem(documentBloc),
referencedGridSlashMenuItem, referencedGridSlashMenuItem,

View File

@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -31,16 +32,19 @@ class BlockAddButton extends StatelessWidget {
children: [ children: [
TextSpan( TextSpan(
text: LocaleKeys.blockActions_addBelowTooltip.tr(), text: LocaleKeys.blockActions_addBelowTooltip.tr(),
style: context.tooltipTextStyle(),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: Platform.isMacOS text: Platform.isMacOS
? LocaleKeys.blockActions_addAboveMacCmd.tr() ? LocaleKeys.blockActions_addAboveMacCmd.tr()
: LocaleKeys.blockActions_addAboveCmd.tr(), : LocaleKeys.blockActions_addAboveCmd.tr(),
style: context.tooltipTextStyle(),
), ),
const TextSpan(text: ' '), const TextSpan(text: ' '),
TextSpan( TextSpan(
text: LocaleKeys.blockActions_addAboveTooltip.tr(), text: LocaleKeys.blockActions_addAboveTooltip.tr(),
style: context.tooltipTextStyle(),
), ),
], ],
), ),

View File

@ -21,7 +21,6 @@ class BlockActionButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Align(
child: FlowyTooltip( child: FlowyTooltip(
preferBelow: false,
richMessage: richMessage, richMessage: richMessage,
child: MouseRegion( child: MouseRegion(
cursor: Platform.isWindows cursor: Platform.isWindows

View File

@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -67,11 +68,14 @@ class BlockOptionButton extends StatelessWidget {
controller.close(); controller.close();
} }
}, },
buildChild: (controller) => _buildOptionButton(controller), buildChild: (controller) => _buildOptionButton(context, controller),
); );
} }
Widget _buildOptionButton(PopoverController controller) { Widget _buildOptionButton(
BuildContext context,
PopoverController controller,
) {
return BlockActionButton( return BlockActionButton(
svg: FlowySvgs.drag_element_s, svg: FlowySvgs.drag_element_s,
richMessage: TextSpan( richMessage: TextSpan(
@ -79,9 +83,11 @@ class BlockOptionButton extends StatelessWidget {
TextSpan( TextSpan(
// todo: customize the color to highlight the text. // todo: customize the color to highlight the text.
text: LocaleKeys.document_plugins_optionAction_click.tr(), text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
), ),
TextSpan( TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
), ),
], ],
), ),

View File

@ -100,9 +100,7 @@ class ClipboardService {
for (final item in reader.items) { for (final item in reader.items) {
final availableFormats = await item.rawReader!.getAvailableFormats(); final availableFormats = await item.rawReader!.getAvailableFormats();
Log.debug( Log.info('availableFormats: $availableFormats');
'availableFormats: $availableFormats',
);
} }
final plainText = await reader.readValue(Formats.plainText); final plainText = await reader.readValue(Formats.plainText);
@ -115,6 +113,8 @@ class ClipboardService {
image = ('jpeg', await reader.readFile(Formats.jpeg)); image = ('jpeg', await reader.readFile(Formats.jpeg));
} else if (reader.canProvide(Formats.gif)) { } else if (reader.canProvide(Formats.gif)) {
image = ('gif', await reader.readFile(Formats.gif)); image = ('gif', await reader.readFile(Formats.gif));
} else if (reader.canProvide(Formats.webp)) {
image = ('webp', await reader.readFile(Formats.webp));
} }
return ClipboardServiceData( return ClipboardServiceData(

View File

@ -76,6 +76,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
image.$1, image.$1,
image.$2!, image.$2!,
documentId, documentId,
selection: selection,
); );
if (result) { if (result) {
Log.info('Pasted image'); Log.info('Pasted image');

View File

@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/imag
import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
@ -23,6 +23,7 @@ extension PasteFromImage on EditorState {
'png', 'png',
'jpeg', 'jpeg',
'gif', 'gif',
'webp',
]; ];
Future<void> dropImages( Future<void> dropImages(
@ -34,7 +35,7 @@ extension PasteFromImage on EditorState {
final imageFiles = files.where( final imageFiles = files.where(
(file) => (file) =>
file.mimeType?.startsWith('image/') ?? file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name), false || imgExtensionRegex.hasMatch(file.name.toLowerCase()),
); );
for (final file in imageFiles) { for (final file in imageFiles) {
@ -64,18 +65,26 @@ extension PasteFromImage on EditorState {
Future<bool> pasteImage( Future<bool> pasteImage(
String format, String format,
Uint8List imageBytes, Uint8List imageBytes,
String documentId, String documentId, {
) async { Selection? selection,
if (!supportedImageFormats.contains(format)) { }) async {
return false;
}
final context = document.root.context; final context = document.root.context;
if (context == null) { if (context == null) {
return false; return false;
} }
if (!supportedImageFormats.contains(format)) {
Log.info('unsupported format: $format');
if (PlatformExtension.isMobile) {
showToastNotification(
context,
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
);
}
return false;
}
final isLocalMode = context.read<DocumentBloc>().isLocalMode; final isLocalMode = context.read<DocumentBloc>().isLocalMode;
final path = await getIt<ApplicationDataStorage>().getPath(); final path = await getIt<ApplicationDataStorage>().getPath();
@ -105,9 +114,9 @@ extension PasteFromImage on EditorState {
final errorMessage = result.$2; final errorMessage = result.$2;
if (errorMessage != null && context.mounted) { if (errorMessage != null && context.mounted) {
showSnackBarMessage( showToastNotification(
context, context,
errorMessage, message: errorMessage,
); );
return false; return false;
} }
@ -116,7 +125,7 @@ extension PasteFromImage on EditorState {
} }
if (path != null) { if (path != null) {
await insertImageNode(path); await insertImageNode(path, selection: selection);
} }
await File(copyToPath).delete(); await File(copyToPath).delete();
@ -124,13 +133,55 @@ extension PasteFromImage on EditorState {
} catch (e) { } catch (e) {
Log.error('cannot copy image file', e); Log.error('cannot copy image file', e);
if (context.mounted) { if (context.mounted) {
showSnackBarMessage( showToastNotification(
context, context,
LocaleKeys.document_imageBlock_error_invalidImage.tr(), message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
); );
} }
} }
return false; return false;
} }
Future<void> insertImageNode(
String src, {
Selection? selection,
}) async {
selection ??= this.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final transaction = this.transaction;
// if the current node is empty paragraph, replace it with image node
if (node.type == ParagraphBlockKeys.type &&
(node.delta?.isEmpty ?? false)) {
transaction
..insertNode(
node.path,
imageNode(
url: src,
),
)
..deleteNode(node);
} else {
transaction.insertNode(
node.path.next,
imageNode(
url: src,
),
);
}
transaction.afterSelection = Selection.collapsed(
Position(
path: node.path.next,
),
);
return apply(transaction);
}
} }

View File

@ -34,10 +34,12 @@ class DocumentImmersiveCover extends StatefulWidget {
super.key, super.key,
required this.view, required this.view,
required this.userProfilePB, required this.userProfilePB,
this.fixedTitle,
}); });
final ViewPB view; final ViewPB view;
final UserProfilePB userProfilePB; final UserProfilePB userProfilePB;
final String? fixedTitle;
@override @override
State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState(); State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();
@ -143,6 +145,18 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily; fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily;
} }
if (widget.fixedTitle != null) {
return FlowyText(
widget.fixedTitle!,
fontSize: 28.0,
fontWeight: FontWeight.w700,
fontFamily: fontFamily,
color:
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
overflow: TextOverflow.ellipsis,
);
}
return AutoSizeTextField( return AutoSizeTextField(
controller: textEditingController, controller: textEditingController,
focusNode: focusNode, focusNode: focusNode,

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_extension.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/dispatch/error.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -39,14 +38,6 @@ Future<(String? path, String? errorMessage)> saveFileToCloudStorage(
String localFilePath, String localFilePath,
String documentId, String documentId,
) async { ) async {
final size = localFilePath.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
// 10MB
return (
null,
LocaleKeys.document_plugins_file_fileTooBigError.tr(),
);
}
final documentService = DocumentService(); final documentService = DocumentService();
Log.debug("Uploading file from local path: $localFilePath"); Log.debug("Uploading file from local path: $localFilePath");
final result = await documentService.uploadFile( final result = await documentService.uploadFile(

View File

@ -7,7 +7,6 @@ import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_extension.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/dispatch/error.dart';
@ -47,14 +46,6 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
String localImagePath, String localImagePath,
String documentId, String documentId,
) async { ) async {
final size = localImagePath.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
// 10MB
return (
null,
LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(),
);
}
final documentService = DocumentService(); final documentService = DocumentService();
Log.debug("Uploading image local path: $localImagePath"); Log.debug("Uploading image local path: $localImagePath");
final result = await documentService.uploadFile( final result = await documentService.uploadFile(

View File

@ -121,38 +121,36 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
final type = values[currentTabIndex]; final type = values[currentTabIndex];
switch (type) { switch (type) {
case UploadImageType.local: case UploadImageType.local:
return Column( Widget child = UploadImageFileWidget(
children: [ allowMultipleImages: widget.allowMultipleImages,
Padding( onPickFiles: widget.onSelectedLocalImages,
padding: const EdgeInsets.all(8.0), );
child: Container( if (PlatformExtension.isDesktop) {
alignment: Alignment.center, child = Padding(
decoration: BoxDecoration( padding: const EdgeInsets.all(8.0),
borderRadius: BorderRadius.circular(8), child: Container(
border: Border.all( alignment: Alignment.center,
color: Theme.of(context).colorScheme.outline, decoration: BoxDecoration(
), borderRadius: BorderRadius.circular(8),
), border: Border.all(
constraints: constraints, color: Theme.of(context).colorScheme.outline,
child: Column(
children: [
UploadImageFileWidget(
allowMultipleImages: widget.allowMultipleImages,
onPickFiles: widget.onSelectedLocalImages,
),
],
), ),
), ),
constraints: constraints,
child: child,
), ),
// if (widget.limitMaximumImageSize) ...[ );
// FlowyText( } else {
// LocaleKeys.document_imageBlock_maximumImageSize.tr(), child = Padding(
// fontSize: 10.0, padding: const EdgeInsets.symmetric(
// color: Theme.of(context).hintColor, horizontal: 8.0,
// ), vertical: 12.0,
// ], ),
], child: child,
); );
}
return child;
case UploadImageType.url: case UploadImageType.url:
return Container( return Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),

Some files were not shown because too many files have changed in this diff Show More