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
jobs:
tauri-build-self-hosted:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
mkdir dist
pnpm install
cd src-tauri && cargo build
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint:tauri
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_web_app
args: "--debug"
# tauri-build-self-hosted:
# if: github.event.pull_request.head.repo.full_name == github.repository
# runs-on: self-hosted
#
# steps:
# - uses: actions/checkout@v4
# - name: install frontend dependencies
# working-directory: frontend/appflowy_web_app
# run: |
# mkdir dist
# pnpm install
# cd src-tauri && cargo build
#
# - name: test and lint
# working-directory: frontend/appflowy_web_app
# run: |
# pnpm run lint:tauri
#
# - uses: tauri-apps/tauri-action@v0
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# tauriScript: pnpm tauri
# projectPath: frontend/appflowy_web_app
# args: "--debug"
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
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/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
// import 'package:appflowy/env/cloud_env.dart';
// import 'package:appflowy/workspace/application/settings/prelude.dart';
// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:integration_test/integration_test.dart';
import '../shared/util.dart';
// import '../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// void main() {
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('supabase auth', () {
testWidgets('sign in with supabase', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
});
// group('supabase auth', () {
// testWidgets('sign in with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// });
testWidgets('sign out with supabase', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
// testWidgets('sign out with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
await tester.logout();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// await tester.logout();
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
tester.expectToSeeGoogleLoginButton();
});
// // Go to the sign in page again
// await tester.pumpAndSettle(const Duration(seconds: 1));
// tester.expectToSeeGoogleLoginButton();
// });
testWidgets('sign in as anonymous', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapSignInAsGuest();
// testWidgets('sign in as anonymous', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapSignInAsGuest();
// should not see the sync setting page when sign in as anonymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// // should not see the sync setting page when sign in as anonymous
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
// // Scroll to sign-out
// await tester.scrollUntilVisible(
// find.byType(SignInOutButton),
// 100,
// scrollable: find.findSettingsScrollable(),
// );
// await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton();
});
// tester.expectToSeeGoogleLoginButton();
// });
// testWidgets('enable encryption', (tester) async {
// await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// // testWidgets('enable encryption', (tester) async {
// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// // await tester.tapGoogleLoginInButton();
// // // Open the setting page and sign out
// // await tester.openSettings();
// // await tester.openSettingsPage(SettingsPage.cloud);
// // // the switch should be off by default
// // tester.assertEnableEncryptSwitchValue(false);
// // await tester.toggleEnableEncrypt();
// // // the switch should be on after toggling
// // tester.assertEnableEncryptSwitchValue(true);
// // // the switch can not be toggled back to off
// // await tester.toggleEnableEncrypt();
// // tester.assertEnableEncryptSwitchValue(true);
// // });
// testWidgets('enable sync', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.cloud);
// // the switch should be off by default
// tester.assertEnableEncryptSwitchValue(false);
// await tester.toggleEnableEncrypt();
// // the switch should be on by default
// tester.assertSupabaseEnableSyncSwitchValue(true);
// await tester.toggleEnableSync(SupabaseEnableSync);
// // the switch should be off
// tester.assertSupabaseEnableSyncSwitchValue(false);
// // the switch should be on after toggling
// tester.assertEnableEncryptSwitchValue(true);
// // the switch can not be toggled back to off
// await tester.toggleEnableEncrypt();
// tester.assertEnableEncryptSwitchValue(true);
// await tester.toggleEnableSync(SupabaseEnableSync);
// tester.assertSupabaseEnableSyncSwitchValue(true);
// });
testWidgets('enable sync', (tester) async {
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.cloud);
// the switch should be on by default
tester.assertSupabaseEnableSyncSwitchValue(true);
await tester.toggleEnableSync(SupabaseEnableSync);
// the switch should be off
tester.assertSupabaseEnableSyncSwitchValue(false);
// the switch should be on after toggling
await tester.toggleEnableSync(SupabaseEnableSync);
tester.assertSupabaseEnableSyncSwitchValue(true);
});
});
}
// });
// }

View File

@ -47,13 +47,12 @@ void main() {
await tester.openSettingsPage(SettingsPage.account);
await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
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,
@ -72,6 +71,4 @@ void main() {
expect(profileSetting.name, name);
});
});
}

View File

@ -461,22 +461,22 @@ void main() {
tester.assertChecklistEditorVisible(visible: true);
// 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
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
name: "task 1",
isChecked: false,
);
// 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
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
name: "task 11",
isChecked: false,
);

View File

@ -176,6 +176,7 @@ Future<void> createInlineDatabase(
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.slashMenuName,
offset: 100,
);
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/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -52,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester {
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) {
assertToggleValue(
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 {
final finder = find.descendant(
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/user/application/auth/af_cloud_mock_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/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
@ -55,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester {
switch (cloudType) {
case AuthenticatorType.local:
break;
case AuthenticatorType.supabase:
break;
case AuthenticatorType.appflowyCloudSelfHost:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
@ -75,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester {
case AuthenticatorType.local:
await useLocalServer();
break;
case AuthenticatorType.supabase:
await useTestSupabaseCloud();
getIt.unregister<AuthService>();
getIt.registerFactory<AuthService>(
() => SupabaseMockAuthService(),
);
break;
case AuthenticatorType.appflowyCloudSelfHost:
await useTestSelfHostedAppFlowyCloud();
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 {
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
}

View File

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

View File

@ -63,12 +63,15 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- printing (1.0.0):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (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):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -100,7 +103,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- 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`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@ -113,6 +116,7 @@ SPEC REPOS:
- DKPhotoGallery
- ReachabilitySwift
- SDWebImage
- Sentry
- SwiftyGif
- Toast
@ -147,8 +151,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
printing:
:path: ".symlinks/plugins/printing/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@ -170,7 +174,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
@ -178,9 +182,10 @@ SPEC CHECKSUMS:
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
@ -191,4 +196,4 @@ SPEC CHECKSUMS:
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.platform,
required this.authenticator_type,
required this.supabase_config,
required this.appflowy_cloud_config,
required this.envs,
});
@ -28,41 +27,12 @@ class AppFlowyConfiguration {
final String device_id;
final String platform;
final int authenticator_type;
final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config;
final Map<String, String> envs;
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()
class AppFlowyCloudConfiguration {
AppFlowyCloudConfiguration({

View File

@ -21,9 +21,6 @@ Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
case AuthenticatorType.local:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
break;
case AuthenticatorType.supabase:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
break;
case AuthenticatorType.appflowyCloud:
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
break;
@ -63,8 +60,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
switch (value ?? "0") {
case "0":
return AuthenticatorType.local;
case "1":
return AuthenticatorType.supabase;
case "2":
return AuthenticatorType.appflowyCloud;
case "3":
@ -93,10 +88,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
/// Returns `false` otherwise.
bool get isAuthEnabled {
final env = getIt<AppFlowyCloudSharedEnv>();
if (env.authenticatorType == AuthenticatorType.supabase) {
return env.supabaseConfig.isValid;
}
if (env.authenticatorType.isAppFlowyCloudEnabled) {
return env.appflowyCloudConfig.isValid;
}
@ -104,19 +95,6 @@ bool get isAuthEnabled {
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.
bool get isAppFlowyCloudEnabled {
return currentCloudType().isAppFlowyCloudEnabled;
@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled {
enum AuthenticatorType {
local,
supabase,
appflowyCloud,
appflowyCloudSelfHost,
// The 'appflowyCloudDevelop' type is used for develop purposes only.
@ -137,14 +114,10 @@ enum AuthenticatorType {
this == AuthenticatorType.appflowyCloudDevelop ||
this == AuthenticatorType.appflowyCloud;
bool get isSupabaseEnabled => this == AuthenticatorType.supabase;
int get value {
switch (this) {
case AuthenticatorType.local:
return 0;
case AuthenticatorType.supabase:
return 1;
case AuthenticatorType.appflowyCloud:
return 2;
case AuthenticatorType.appflowyCloudSelfHost:
@ -158,8 +131,6 @@ enum AuthenticatorType {
switch (value) {
case 0:
return AuthenticatorType.local;
case 1:
return AuthenticatorType.supabase;
case 2:
return AuthenticatorType.appflowyCloud;
case 3:
@ -197,25 +168,15 @@ Future<void> useLocalServer() async {
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.
class AppFlowyCloudSharedEnv {
AppFlowyCloudSharedEnv({
required AuthenticatorType authenticatorType,
required this.appflowyCloudConfig,
required this.supabaseConfig,
}) : _authenticatorType = authenticatorType;
final AuthenticatorType _authenticatorType;
final AppFlowyCloudConfiguration appflowyCloudConfig;
final SupabaseConfiguration supabaseConfig;
AuthenticatorType get authenticatorType => _authenticatorType;
@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv {
? await getAppFlowyCloudConfig(authenticatorType)
: 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,
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv(
authenticatorType: authenticatorType,
appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: supabaseCloudConfig,
);
} else {
// Using the cloud settings from the .env file.
@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv(
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: SupabaseConfiguration.defaultConfig(),
);
}
}
@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv {
@override
String toString() {
return 'authenticator: $_authenticatorType\n'
'appflowy: ${appflowyCloudConfig.toJson()}\n'
'supabase: ${supabaseConfig.toJson()})\n';
'appflowy: ${appflowyCloudConfig.toJson()}\n';
}
}
@ -354,22 +308,3 @@ Future<void> setSupabaseServer(
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: '',
)
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 '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/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/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:flutter/material.dart';
import 'package:go_router/go_router.dart';
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
getIt<MenuSharedState>().latestOpenView = view;
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(
path: view.routeName,
queryParameters: view.queryParameters(arguments),
queryParameters: queryParameters,
).toString();
await push(uri);
}

View File

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

View File

@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
UserProfileBloc() : super(const _Initial()) {
on<UserProfileEvent>((event, emit) async {
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());
final workspaceOrFailure =

View File

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

View File

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

View File

@ -116,12 +116,18 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
leftButton: FlowyText.regular(
leftButton: FlowyText(
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(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
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_actions.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/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@ -294,6 +295,7 @@ class MobileRowDetailPageContentState
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
ValueNotifier<String> primaryFieldId = ValueNotifier('');
@override
void initState() {
@ -326,7 +328,13 @@ class MobileRowDetailPageContentState
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..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) {
if (state.primaryField == null) {
return const SizedBox.shrink();
@ -366,6 +374,23 @@ class MobileRowDetailPageContentState
if (rowDetailState.numHiddenFields != 0) ...[
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(
viewId: viewId,
fieldController: fieldController,

View File

@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: GridSize.headerHeight,
maxHeight: GridSize.headerHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory,
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(vertical: 14, horizontal: 6),
EdgeInsets.symmetric(horizontal: 6, vertical: 2),
),
),
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,
required this.id,
this.title,
this.showMoreButton = true,
this.fixedTitle,
});
/// view id
final String id;
final String? title;
final bool showMoreButton;
final String? fixedTitle;
static const routeName = '/docs';
static const viewId = 'id';
static const viewTitle = 'title';
static const viewShowMoreButton = 'show_more_button';
static const viewFixedTitle = 'fixed_title';
@override
Widget build(BuildContext context) {
@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget {
id: id,
title: title,
viewLayout: ViewLayoutPB.Document,
showMoreButton: showMoreButton,
fixedTitle: fixedTitle,
);
}
}

View File

@ -96,8 +96,7 @@ class _FavoriteViews extends StatelessWidget {
final borderColor = Theme.of(context).isLightMode
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return Scrollbar(
child: ListView.separated(
return ListView.separated(
key: const PageStorageKey('favorite_views_page_storage_key'),
padding: EdgeInsets.only(
bottom: HomeSpaceViewSizes.mVerticalPadding +
@ -125,7 +124,6 @@ class _FavoriteViews extends StatelessWidget {
},
separatorBuilder: (context, index) => const HSpace(8),
itemCount: favoriteViews.length,
),
);
}
}

View File

@ -25,8 +25,7 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
'';
return Scrollbar(
child: SingleChildScrollView(
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(
top: HomeSpaceViewSizes.mVerticalPadding,
@ -39,7 +38,6 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
showFavorite: false,
),
),
),
);
}
}

View File

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

View File

@ -1,8 +1,10 @@
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/tab/mobile_space_tab.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/user/application/auth/auth_service.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/home_sizes.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/log.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-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_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:sentry/sentry.dart';
import 'package:toastification/toastification.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@ -59,6 +66,14 @@ class MobileHomeScreen extends StatelessWidget {
return const WorkspaceFailedScreen();
}
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: userProfile.id.toString(),
),
),
);
return Scaffold(
body: SafeArea(
bottom: false,
@ -94,6 +109,8 @@ class MobileHomePage extends StatefulWidget {
}
class _MobileHomePageState extends State<MobileHomePage> {
Loading? loadingIndicator;
@override
void initState() {
super.initState();
@ -125,13 +142,42 @@ class _MobileHomePageState extends State<MobileHomePage> {
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
),
],
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
child: _HomePage(userProfile: widget.userProfile),
);
}
void _onLatestViewChange() async {
final id = getIt<MenuSharedState>().latestOpenView?.id;
if (id == null) {
return;
}
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) {
@ -141,6 +187,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
final workspaceId = state.currentWorkspace!.workspaceId;
return Column(
key: ValueKey('mobile_home_page_$workspaceId'),
children: [
// Header
Padding(
@ -158,8 +205,8 @@ class _MobileHomePageState extends State<MobileHomePage> {
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SpaceOrderBloc()
..add(const SpaceOrderEvent.initial()),
create: (_) =>
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
),
BlocProvider(
create: (_) => SidebarSectionsBloc()
@ -193,15 +240,61 @@ class _MobileHomePageState extends State<MobileHomePage> {
],
);
},
),
);
}
void _onLatestViewChange() async {
final id = getIt<MenuSharedState>().latestOpenView?.id;
if (id == null) {
void _showResultDialog(BuildContext context, UserWorkspaceState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
}
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
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/setting/cloud/cloud_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/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
@ -79,8 +80,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
PersonalInfoSettingGroup(
userProfile: userProfile,
),
// TODO: Enable and implement along with Push Notifications
// const NotificationsSettingGroup(),
const WorkspaceSettingGroup(),
const AppearanceSettingGroup(),
const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(),

View File

@ -68,7 +68,6 @@ class _RecentViews extends StatelessWidget {
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return SlidableAutoCloseBehavior(
child: Scrollbar(
child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'),
padding: EdgeInsets.only(
@ -98,7 +97,6 @@ class _RecentViews extends StatelessWidget {
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/space_order_bloc.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/sidebar/folder/folder_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-user/protobuf.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:provider/provider.dart';
import 'ai_bubble_button.dart';
final ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);
class MobileSpaceTab extends StatefulWidget {
const MobileSpaceTab({
super.key,
@ -37,14 +44,19 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
void initState() {
super.initState();
mobileCreateNewPageNotifier.addListener(_createNewPage);
mobileCreateNewPageNotifier.addListener(_createNewDocument);
mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);
mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);
}
@override
void dispose() {
tabController?.removeListener(_onTabChange);
tabController?.dispose();
mobileCreateNewPageNotifier.removeListener(_createNewPage);
mobileCreateNewPageNotifier.removeListener(_createNewDocument);
mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);
mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);
super.dispose();
}
@ -140,7 +152,20 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
case MobileSpaceTabType.recent:
return const MobileRecentSpace();
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:
return MobileFavoriteSpace(userProfile: widget.userProfile);
default:
@ -150,15 +175,24 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
}
// 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) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
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(
SidebarSectionsEvent.createRootViewInSection(
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/startup/startup.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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -74,10 +75,14 @@ class SupportSettingGroup extends StatelessWidget {
actionButtonTitle: LocaleKeys.button_yes.tr(),
onActionButtonPressed: () async {
await getIt<FlowyCacheManager>().clearAllCache();
// check the workspace and space health
await WorkspaceDataManager.checkViewHealth(
dryRun: false,
);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys.settings_files_clearCacheSuccess.tr(),
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
);
}
},

View File

@ -42,14 +42,24 @@ class UserSessionSettingGroup extends StatelessWidget {
MobileSignInOrLogoutButton(
labelText: LocaleKeys.settings_menu_logout.tr(),
onPressed: () async {
await showFlowyMobileConfirmDialog(
context,
content: FlowyText(
LocaleKeys.settings_menu_logoutPrompt.tr(),
await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.settings_menu_logoutPrompt.tr(),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.error,
onActionButtonPressed: () async {
rightButton: FlowyText(
LocaleKeys.button_logout.tr(),
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 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(
context: context ?? AppGlobals.context,
barrierColor: Colors.black.withOpacity(0.25),
builder: (context) => CupertinoAlertDialog(
title: FlowyText.medium(
title,
fontSize: 18,
fontSize: 16,
maxLines: 10,
lineHeight: 1.3,
figmaLineHeight: 22.0,
),
actions: [
CupertinoDialogAction(

View File

@ -425,6 +425,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
},
),
const VSpace(6),
if (PlatformExtension.isDesktop)
Opacity(
opacity: 0.6,
child: FlowyText(

View File

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

View File

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

View File

@ -81,6 +81,12 @@ class RowCache {
_changedNotifier.receive(const ChangedReason.setInitialRows());
}
void setRowMeta(RowMetaPB rowMeta) {
final rowInfo = buildGridRow(rowMeta);
_rowList.add(rowInfo);
_changedNotifier.receive(const ChangedReason.didFetchRow());
}
void dispose() {
_rowLifeCycle.onRowDisposed();
_changedNotifier.dispose();
@ -215,7 +221,8 @@ class RowCache {
if (rowInfo == null) {
_loadRow(rowMeta.id);
}
return _makeCells(rowMeta);
final cells = _makeCells(rowMeta);
return cells;
}
Future<void> _loadRow(RowId rowId) async {
@ -277,6 +284,7 @@ class RowChangesetNotifier extends ChangeNotifier {
reorderRows: (_) => notifyListeners(),
reorderSingleRow: (_) => notifyListeners(),
setInitialRows: (_) => notifyListeners(),
didFetchRow: (_) => notifyListeners(),
);
}
}
@ -305,6 +313,7 @@ class ChangedReason with _$ChangedReason {
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
const factory ChangedReason.initial() = InitialListState;
const factory ChangedReason.didFetchRow() = _DidFetchRow;
const factory ChangedReason.reorderRows() = _ReorderRows;
const factory ChangedReason.reorderSingleRow(
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:flutter/material.dart';
@ -9,35 +11,60 @@ typedef OnRowChanged = void Function(List<CellContext>, ChangedReason);
class RowController {
RowController({
required this.rowMeta,
required RowMetaPB rowMeta,
required this.viewId,
required RowCache rowCache,
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 viewId;
final List<VoidCallback> _onRowChangedListeners = [];
final RowCache _rowCache;
final RowListener _rowListener;
final RowBackendService _rowBackendSvc;
bool _isDisposed = false;
CellMemCache get cellCache => _rowCache.cellCache;
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}) {
final fn = _rowCache.addListener(
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.
_onRowChangedListeners.add(fn);
}
void dispose() {
Future<void> dispose() async {
_isDisposed = true;
await _rowListener.stop();
for (final fn in _onRowChangedListeners) {
_rowCache.removeRowListener(fn);
}

View File

@ -37,6 +37,14 @@ class RowBackendService {
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) {
return createRow(
viewId: viewId,

View File

@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:collection';
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/protobuf/flowy-database2/protobuf.dart';
import '../defines.dart';
import '../field/field_controller.dart';
@ -91,6 +93,17 @@ class DatabaseViewCache {
(reorderRow) => _rowCache.reorderSingleRow(reorderRow),
(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(

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@override
Future<void> close() async {
rowController.dispose();
await rowController.dispose();
return super.close();
}
@ -125,7 +125,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
}
void _init() {
allCells.addAll(rowController.loadData());
allCells.addAll(rowController.loadCells());
int numHiddenFields = 0;
final visibleCells = <CellContext>[];
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_backend/log.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:flowy_infra_ui/flowy_infra_ui.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(
(_) => GridShortcuts(
child: GridPageContent(
key: ValueKey(widget.view.id),
view: widget.view,
),
),
@ -331,33 +331,10 @@ class _GridRowsState extends State<_GridRows> {
BuildContext context,
GridState state,
) {
final children = state.rowInfos.mapIndexed((index, rowInfo) {
return _renderRow(
context,
rowInfo.rowId,
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));
// 1. GridRowBottomBar
// 2. GridCalculationsRow
// 3. Footer Padding
final itemCount = state.rowInfos.length + 3;
return Stack(
children: [
@ -381,8 +358,37 @@ class _GridRowsState extends State<_GridRows> {
.add(GridEvent.moveRow(fromIndex, toIndex));
}
},
itemCount: children.length,
itemBuilder: (context, index) => children[index],
itemCount: itemCount,
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) ...[

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,18 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
return Container(
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
child: Column(
children: [
TextField(
controller: textEditingController,
@ -49,6 +60,7 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
],
),
],
),
);
}
}

View File

@ -13,7 +13,18 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
FocusNode focusNode,
TextEditingController textEditingController,
) {
return Column(
return Container(
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 2,
),
child: Column(
children: [
TextField(
readOnly: true,
@ -49,6 +60,7 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
],
),
],
),
);
}
}

View File

@ -1,8 +1,5 @@
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/locale_keys.g.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:flowy_infra/theme_extension.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 '../cell/editable_cell_builder.dart';
import 'accessory/cell_accessory.dart';
/// 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,
richMessage: TextSpan(
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/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/cell_controller.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/row/cells/cell_container.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/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
@ -47,20 +44,16 @@ class ViewTitleBarWithRow extends StatelessWidget {
if (state.ancestors.isEmpty) {
return const SizedBox.shrink();
}
const maxWidth = WindowSizeManager.minWindowWidth - 200;
return LayoutBuilder(
builder: (context, constraints) {
return Visibility(
visible: maxWidth < constraints.maxWidth,
// if the width is too small, only show one view title bar without the ancestors
replacement: _buildRowName(),
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
height: 24,
child: Row(
// 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
return views.length > 2
? [
_buildViewButton(views.first),
const FlowyText.regular('/'),
const FlowyText.regular(' ... /'),
_buildViewButton(views[1]),
const FlowySvg(FlowySvgs.title_bar_divider_s),
const FlowyText.regular(' ... '),
const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildViewButton(views.last),
const FlowyText.regular('/'),
const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildRowName(),
]
: [
...views
.map((e) => [_buildViewButton(e), const FlowyText.regular('/')])
.map(
(e) => [
_buildViewButton(e),
const FlowySvg(FlowySvgs.title_bar_divider_s),
],
)
.flattened,
_buildRowName(),
];
@ -89,9 +88,9 @@ class ViewTitleBarWithRow extends StatelessWidget {
Widget _buildViewButton(ViewPB view) {
return FlowyTooltip(
message: view.name,
child: _ViewTitle(
child: ViewTitle(
view: view,
behavior: _ViewTitleBehavior.uneditable,
behavior: ViewTitleBehavior.uneditable,
onUpdated: () {},
),
);
@ -180,11 +179,14 @@ class _TitleSkin extends IEditableTextCellSkin {
onTap: () {},
text: Row(
children: [
EmojiText(
emoji: state.icon ?? "",
fontSize: 18.0,
if (state.icon != null) ...[
FlowyText.emoji(
state.icon!,
fontSize: 14.0,
figmaLineHeight: 18.0,
),
const HSpace(2.0),
const HSpace(4.0),
],
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
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 {
const RenameRowPopover({
super.key,

View File

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

View File

@ -2,6 +2,7 @@ library document_plugin;
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/document_page.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(
value: bloc,
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
@ -126,6 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
view: view,
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
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/mobile/application/page_style/document_page_style_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
@ -42,11 +41,13 @@ class DocumentPage extends StatefulWidget {
required this.view,
required this.onDeleted,
this.initialSelection,
this.fixedTitle,
});
final ViewPB view;
final VoidCallback onDeleted;
final Selection? initialSelection;
final String? fixedTitle;
@override
State<DocumentPage> createState() => _DocumentPageState();
@ -103,6 +104,7 @@ class _DocumentPageState extends State<DocumentPage>
BlocProvider.value(value: documentBloc),
],
child: BlocBuilder<DocumentBloc, DocumentState>(
buildWhen: _shouldRebuildDocument,
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator.adaptive());
@ -195,13 +197,15 @@ class _DocumentPageState extends State<DocumentPage>
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
final List<XFile> imageFiles = [];
final List<XFile> otherfiles = [];
final List<XFile> otherFiles = [];
for (final file in details.files) {
final fileName = file.name.toLowerCase();
if (file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name)) {
false || imgExtensionRegex.hasMatch(fileName)) {
imageFiles.add(file);
} else {
otherfiles.add(file);
otherFiles.add(file);
}
}
@ -213,7 +217,7 @@ class _DocumentPageState extends State<DocumentPage>
);
await editorState!.dropFiles(
data.dropTarget!,
otherfiles,
otherFiles,
widget.view.id,
isLocalMode,
);
@ -261,6 +265,7 @@ class _DocumentPageState extends State<DocumentPage>
if (PlatformExtension.isMobile) {
return DocumentImmersiveCover(
fixedTitle: widget.fixedTitle,
view: widget.view,
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,
bulletedListSlashMenuItem,
numberedListSlashMenuItem,
todoListSlashMenuItem,
dividerSlashMenuItem,
quoteSlashMenuItem,
tableSlashMenuItem,
referencedDocSlashMenuItem,
gridSlashMenuItem(documentBloc),
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_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -31,16 +32,19 @@ class BlockAddButton extends StatelessWidget {
children: [
TextSpan(
text: LocaleKeys.blockActions_addBelowTooltip.tr(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: '\n'),
TextSpan(
text: Platform.isMacOS
? LocaleKeys.blockActions_addAboveMacCmd.tr()
: LocaleKeys.blockActions_addAboveCmd.tr(),
style: context.tooltipTextStyle(),
),
const TextSpan(text: ' '),
TextSpan(
text: LocaleKeys.blockActions_addAboveTooltip.tr(),
style: context.tooltipTextStyle(),
),
],
),

View File

@ -21,7 +21,6 @@ class BlockActionButton extends StatelessWidget {
Widget build(BuildContext context) {
return Align(
child: FlowyTooltip(
preferBelow: false,
richMessage: richMessage,
child: MouseRegion(
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_popover/appflowy_popover.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';
@ -67,11 +68,14 @@ class BlockOptionButton extends StatelessWidget {
controller.close();
}
},
buildChild: (controller) => _buildOptionButton(controller),
buildChild: (controller) => _buildOptionButton(context, controller),
);
}
Widget _buildOptionButton(PopoverController controller) {
Widget _buildOptionButton(
BuildContext context,
PopoverController controller,
) {
return BlockActionButton(
svg: FlowySvgs.drag_element_s,
richMessage: TextSpan(
@ -79,9 +83,11 @@ class BlockOptionButton extends StatelessWidget {
TextSpan(
// todo: customize the color to highlight the text.
text: LocaleKeys.document_plugins_optionAction_click.tr(),
style: context.tooltipTextStyle(),
),
TextSpan(
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
style: context.tooltipTextStyle(),
),
],
),

View File

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

View File

@ -76,6 +76,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
image.$1,
image.$2!,
documentId,
selection: selection,
);
if (result) {
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/startup/startup.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_editor/appflowy_editor.dart' hide Log;
import 'package:cross_file/cross_file.dart';
@ -23,6 +23,7 @@ extension PasteFromImage on EditorState {
'png',
'jpeg',
'gif',
'webp',
];
Future<void> dropImages(
@ -34,7 +35,7 @@ extension PasteFromImage on EditorState {
final imageFiles = files.where(
(file) =>
file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name),
false || imgExtensionRegex.hasMatch(file.name.toLowerCase()),
);
for (final file in imageFiles) {
@ -64,18 +65,26 @@ extension PasteFromImage on EditorState {
Future<bool> pasteImage(
String format,
Uint8List imageBytes,
String documentId,
) async {
if (!supportedImageFormats.contains(format)) {
return false;
}
String documentId, {
Selection? selection,
}) async {
final context = document.root.context;
if (context == null) {
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 path = await getIt<ApplicationDataStorage>().getPath();
@ -105,9 +114,9 @@ extension PasteFromImage on EditorState {
final errorMessage = result.$2;
if (errorMessage != null && context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
errorMessage,
message: errorMessage,
);
return false;
}
@ -116,7 +125,7 @@ extension PasteFromImage on EditorState {
}
if (path != null) {
await insertImageNode(path);
await insertImageNode(path, selection: selection);
}
await File(copyToPath).delete();
@ -124,13 +133,55 @@ extension PasteFromImage on EditorState {
} catch (e) {
Log.error('cannot copy image file', e);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
);
}
}
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,
required this.view,
required this.userProfilePB,
this.fixedTitle,
});
final ViewPB view;
final UserProfilePB userProfilePB;
final String? fixedTitle;
@override
State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();
@ -143,6 +145,18 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
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(
controller: textEditingController,
focusNode: focusNode,

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_service.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_backend/dispatch/error.dart';
import 'package:appflowy_backend/log.dart';
@ -39,14 +38,6 @@ Future<(String? path, String? errorMessage)> saveFileToCloudStorage(
String localFilePath,
String documentId,
) async {
final size = localFilePath.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
// 10MB
return (
null,
LocaleKeys.document_plugins_file_fileTooBigError.tr(),
);
}
final documentService = DocumentService();
Log.debug("Uploading file from local path: $localFilePath");
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/shared/custom_image_cache_manager.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/presentation/home/toast.dart';
import 'package:appflowy_backend/dispatch/error.dart';
@ -47,14 +46,6 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
String localImagePath,
String documentId,
) async {
final size = localImagePath.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
// 10MB
return (
null,
LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(),
);
}
final documentService = DocumentService();
Log.debug("Uploading image local path: $localImagePath");
final result = await documentService.uploadFile(

View File

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

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