Merge branch 'AppFlowy-IO:main' into main
52
.github/workflows/tauri2_ci.yaml
vendored
@ -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:
|
||||
|
After Width: | Height: | Size: 44 KiB |
@ -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>
|
@ -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>
|
@ -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>
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
12
frontend/appflowy_flutter/dart_dependency_validator.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# dart_dependency_validator.yaml
|
||||
|
||||
allow_pins: true
|
||||
|
||||
include:
|
||||
- "lib/**"
|
||||
|
||||
exclude:
|
||||
- "packages/**"
|
||||
|
||||
ignore:
|
||||
- analyzer
|
@ -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);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
// // 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);
|
||||
// // // 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 off by default
|
||||
// // tester.assertEnableEncryptSwitchValue(false);
|
||||
// // await tester.toggleEnableEncrypt();
|
||||
|
||||
// // the switch should be on after toggling
|
||||
// tester.assertEnableEncryptSwitchValue(true);
|
||||
// // // 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);
|
||||
// });
|
||||
// // // 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();
|
||||
// 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);
|
||||
// // 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 on by default
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
|
||||
// the switch should be off
|
||||
tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||
// // the switch should be off
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||
|
||||
// the switch should be on after toggling
|
||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
// // the switch should be on after toggling
|
||||
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -176,6 +176,7 @@ Future<void> createInlineDatabase(
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
layout.slashMenuName,
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
67
frontend/appflowy_flutter/lib/env/cloud_env.dart
vendored
@ -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 ?? '';
|
||||
}
|
||||
|
7
frontend/appflowy_flutter/lib/env/env.dart
vendored
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -425,6 +425,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
},
|
||||
),
|
||||
const VSpace(6),
|
||||
if (PlatformExtension.isDesktop)
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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) ...[
|
||||
|
@ -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;
|
||||
|
@ -81,7 +81,7 @@ class _MobileGridRowState extends State<MobileGridRow> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_rowController.dispose();
|
||||
await _rowController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -39,6 +39,10 @@ class DocumentCollaboratorsBloc
|
||||
if (userProfile != null) {
|
||||
_listener.start(
|
||||
onDocAwarenessUpdate: (states) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
add(
|
||||
DocumentCollaboratorsEvent.update(
|
||||
userProfile,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -419,7 +419,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
imageSlashMenuItem,
|
||||
bulletedListSlashMenuItem,
|
||||
numberedListSlashMenuItem,
|
||||
todoListSlashMenuItem,
|
||||
dividerSlashMenuItem,
|
||||
quoteSlashMenuItem,
|
||||
tableSlashMenuItem,
|
||||
referencedDocSlashMenuItem,
|
||||
gridSlashMenuItem(documentBloc),
|
||||
referencedGridSlashMenuItem,
|
||||
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -76,6 +76,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||
image.$1,
|
||||
image.$2!,
|
||||
documentId,
|
||||
selection: selection,
|
||||
);
|
||||
if (result) {
|
||||
Log.info('Pasted image');
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|