Merge branch 'AppFlowy-IO:main' into main
52
.github/workflows/tauri2_ci.yaml
vendored
@ -20,34 +20,34 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tauri-build-self-hosted:
|
# tauri-build-self-hosted:
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
# if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
runs-on: self-hosted
|
# runs-on: self-hosted
|
||||||
|
#
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: install frontend dependencies
|
# - name: install frontend dependencies
|
||||||
working-directory: frontend/appflowy_web_app
|
# working-directory: frontend/appflowy_web_app
|
||||||
run: |
|
# run: |
|
||||||
mkdir dist
|
# mkdir dist
|
||||||
pnpm install
|
# pnpm install
|
||||||
cd src-tauri && cargo build
|
# cd src-tauri && cargo build
|
||||||
|
#
|
||||||
- name: test and lint
|
# - name: test and lint
|
||||||
working-directory: frontend/appflowy_web_app
|
# working-directory: frontend/appflowy_web_app
|
||||||
run: |
|
# run: |
|
||||||
pnpm run lint:tauri
|
# pnpm run lint:tauri
|
||||||
|
#
|
||||||
- uses: tauri-apps/tauri-action@v0
|
# - uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
# env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
# with:
|
||||||
tauriScript: pnpm tauri
|
# tauriScript: pnpm tauri
|
||||||
projectPath: frontend/appflowy_web_app
|
# projectPath: frontend/appflowy_web_app
|
||||||
args: "--debug"
|
# args: "--debug"
|
||||||
|
|
||||||
tauri-build-ubuntu:
|
tauri-build-ubuntu:
|
||||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
#if: github.event.pull_request.head.repo.full_name != github.repository
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
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/env/cloud_env.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
// import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
// import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
// import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import '../shared/util.dart';
|
// import '../shared/util.dart';
|
||||||
|
|
||||||
void main() {
|
// void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('supabase auth', () {
|
// group('supabase auth', () {
|
||||||
testWidgets('sign in with supabase', (tester) async {
|
// testWidgets('sign in with supabase', (tester) async {
|
||||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||||
await tester.tapGoogleLoginInButton();
|
// await tester.tapGoogleLoginInButton();
|
||||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||||
});
|
// });
|
||||||
|
|
||||||
testWidgets('sign out with supabase', (tester) async {
|
// testWidgets('sign out with supabase', (tester) async {
|
||||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||||
await tester.tapGoogleLoginInButton();
|
// await tester.tapGoogleLoginInButton();
|
||||||
|
|
||||||
// Open the setting page and sign out
|
// // Open the setting page and sign out
|
||||||
await tester.openSettings();
|
// await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.account);
|
// await tester.openSettingsPage(SettingsPage.account);
|
||||||
await tester.logout();
|
// await tester.logout();
|
||||||
|
|
||||||
// Go to the sign in page again
|
// // Go to the sign in page again
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
// await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
tester.expectToSeeGoogleLoginButton();
|
// tester.expectToSeeGoogleLoginButton();
|
||||||
});
|
// });
|
||||||
|
|
||||||
testWidgets('sign in as anonymous', (tester) async {
|
// testWidgets('sign in as anonymous', (tester) async {
|
||||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||||
await tester.tapSignInAsGuest();
|
// await tester.tapSignInAsGuest();
|
||||||
|
|
||||||
// should not see the sync setting page when sign in as anonymous
|
// // should not see the sync setting page when sign in as anonymous
|
||||||
await tester.openSettings();
|
// await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.account);
|
// await tester.openSettingsPage(SettingsPage.account);
|
||||||
|
|
||||||
// Scroll to sign-out
|
// // Scroll to sign-out
|
||||||
await tester.scrollUntilVisible(
|
// await tester.scrollUntilVisible(
|
||||||
find.byType(SignInOutButton),
|
// find.byType(SignInOutButton),
|
||||||
100,
|
// 100,
|
||||||
scrollable: find.findSettingsScrollable(),
|
// scrollable: find.findSettingsScrollable(),
|
||||||
);
|
// );
|
||||||
await tester.tapButton(find.byType(SignInOutButton));
|
// await tester.tapButton(find.byType(SignInOutButton));
|
||||||
|
|
||||||
tester.expectToSeeGoogleLoginButton();
|
// tester.expectToSeeGoogleLoginButton();
|
||||||
});
|
// });
|
||||||
|
|
||||||
// testWidgets('enable encryption', (tester) async {
|
// // testWidgets('enable encryption', (tester) async {
|
||||||
// await tester.initializeAppFlowy(cloudType: CloudType.supabase);
|
// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
|
||||||
// await tester.tapGoogleLoginInButton();
|
// // await tester.tapGoogleLoginInButton();
|
||||||
|
|
||||||
// // Open the setting page and sign out
|
// // // Open the setting page and sign out
|
||||||
// await tester.openSettings();
|
// // await tester.openSettings();
|
||||||
// await tester.openSettingsPage(SettingsPage.cloud);
|
// // await tester.openSettingsPage(SettingsPage.cloud);
|
||||||
|
|
||||||
// // the switch should be off by default
|
// // // the switch should be off by default
|
||||||
// tester.assertEnableEncryptSwitchValue(false);
|
// // tester.assertEnableEncryptSwitchValue(false);
|
||||||
// await tester.toggleEnableEncrypt();
|
// // await tester.toggleEnableEncrypt();
|
||||||
|
|
||||||
// // the switch should be on after toggling
|
// // // the switch should be on after toggling
|
||||||
// tester.assertEnableEncryptSwitchValue(true);
|
// // tester.assertEnableEncryptSwitchValue(true);
|
||||||
|
|
||||||
// // the switch can not be toggled back to off
|
// // // the switch can not be toggled back to off
|
||||||
// await tester.toggleEnableEncrypt();
|
// // await tester.toggleEnableEncrypt();
|
||||||
// tester.assertEnableEncryptSwitchValue(true);
|
// // tester.assertEnableEncryptSwitchValue(true);
|
||||||
// });
|
// // });
|
||||||
|
|
||||||
testWidgets('enable sync', (tester) async {
|
// testWidgets('enable sync', (tester) async {
|
||||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||||
await tester.tapGoogleLoginInButton();
|
// await tester.tapGoogleLoginInButton();
|
||||||
|
|
||||||
// Open the setting page and sign out
|
// // Open the setting page and sign out
|
||||||
await tester.openSettings();
|
// await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.cloud);
|
// await tester.openSettingsPage(SettingsPage.cloud);
|
||||||
|
|
||||||
// the switch should be on by default
|
// // the switch should be on by default
|
||||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||||
|
|
||||||
// the switch should be off
|
// // the switch should be off
|
||||||
tester.assertSupabaseEnableSyncSwitchValue(false);
|
// tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||||
|
|
||||||
// the switch should be on after toggling
|
// // the switch should be on after toggling
|
||||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
@ -47,31 +47,28 @@ void main() {
|
|||||||
await tester.openSettingsPage(SettingsPage.account);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
|
|
||||||
await tester.enterUserName(name);
|
await tester.enterUserName(name);
|
||||||
await tester.tapEscButton();
|
|
||||||
|
|
||||||
// wait 2 seconds for the sync to finish
|
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 6));
|
await tester.pumpAndSettle(const Duration(seconds: 6));
|
||||||
});
|
await tester.logout();
|
||||||
|
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
testWidgets('get user icon and name from server', (tester) async {
|
|
||||||
await tester.initializeAppFlowy(
|
|
||||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
|
||||||
email: email,
|
|
||||||
);
|
|
||||||
await tester.tapGoogleLoginInButton();
|
|
||||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.openSettings();
|
|
||||||
await tester.openSettingsPage(SettingsPage.account);
|
|
||||||
|
|
||||||
// Verify name
|
|
||||||
final profileSetting =
|
|
||||||
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
|
||||||
|
|
||||||
expect(profileSetting.name, name);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
testWidgets('get user icon and name from server', (tester) async {
|
||||||
|
await tester.initializeAppFlowy(
|
||||||
|
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||||
|
email: email,
|
||||||
|
);
|
||||||
|
await tester.tapGoogleLoginInButton();
|
||||||
|
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.openSettings();
|
||||||
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
|
|
||||||
|
// Verify name
|
||||||
|
final profileSetting =
|
||||||
|
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||||
|
|
||||||
|
expect(profileSetting.name, name);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -461,22 +461,22 @@ void main() {
|
|||||||
tester.assertChecklistEditorVisible(visible: true);
|
tester.assertChecklistEditorVisible(visible: true);
|
||||||
|
|
||||||
// create a new task with enter
|
// create a new task with enter
|
||||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
await tester.createNewChecklistTask(name: "task 1", enter: true);
|
||||||
|
|
||||||
// assert that the task is displayed
|
// assert that the task is displayed
|
||||||
tester.assertChecklistTaskInEditor(
|
tester.assertChecklistTaskInEditor(
|
||||||
index: 0,
|
index: 0,
|
||||||
name: "task 0",
|
name: "task 1",
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// update the task's name
|
// update the task's name
|
||||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
await tester.renameChecklistTask(index: 0, name: "task 11");
|
||||||
|
|
||||||
// assert that the task's name is updated
|
// assert that the task's name is updated
|
||||||
tester.assertChecklistTaskInEditor(
|
tester.assertChecklistTaskInEditor(
|
||||||
index: 0,
|
index: 0,
|
||||||
name: "task 1",
|
name: "task 11",
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@ Future<void> createInlineDatabase(
|
|||||||
await tester.editor.showSlashMenu();
|
await tester.editor.showSlashMenu();
|
||||||
await tester.editor.tapSlashMenuItemWithName(
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
layout.slashMenuName,
|
layout.slashMenuName,
|
||||||
|
offset: 100,
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -52,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester {
|
|||||||
assert(isSwitched == value);
|
assert(isSwitched == value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void assertEnableEncryptSwitchValue(bool value) {
|
|
||||||
assertSwitchValue(
|
|
||||||
find.descendant(
|
|
||||||
of: find.byType(EnableEncrypt),
|
|
||||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
|
||||||
),
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void assertSupabaseEnableSyncSwitchValue(bool value) {
|
|
||||||
assertSwitchValue(
|
|
||||||
find.descendant(
|
|
||||||
of: find.byType(SupabaseEnableSync),
|
|
||||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
|
||||||
),
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
||||||
assertToggleValue(
|
assertToggleValue(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
@ -82,15 +61,6 @@ extension AppFlowyAuthTest on WidgetTester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleEnableEncrypt() async {
|
|
||||||
final finder = find.descendant(
|
|
||||||
of: find.byType(EnableEncrypt),
|
|
||||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tapButton(finder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> toggleEnableSync(Type syncButton) async {
|
Future<void> toggleEnableSync(Type syncButton) async {
|
||||||
final finder = find.descendant(
|
final finder = find.descendant(
|
||||||
of: find.byType(syncButton),
|
of: find.byType(syncButton),
|
||||||
|
@ -7,7 +7,6 @@ import 'package:appflowy/startup/entry_point.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
|
||||||
import 'package:appflowy/user/presentation/presentation.dart';
|
import 'package:appflowy/user/presentation/presentation.dart';
|
||||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
@ -55,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
switch (cloudType) {
|
switch (cloudType) {
|
||||||
case AuthenticatorType.local:
|
case AuthenticatorType.local:
|
||||||
break;
|
break;
|
||||||
case AuthenticatorType.supabase:
|
|
||||||
break;
|
|
||||||
case AuthenticatorType.appflowyCloudSelfHost:
|
case AuthenticatorType.appflowyCloudSelfHost:
|
||||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||||
@ -75,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
case AuthenticatorType.local:
|
case AuthenticatorType.local:
|
||||||
await useLocalServer();
|
await useLocalServer();
|
||||||
break;
|
break;
|
||||||
case AuthenticatorType.supabase:
|
|
||||||
await useTestSupabaseCloud();
|
|
||||||
getIt.unregister<AuthService>();
|
|
||||||
getIt.registerFactory<AuthService>(
|
|
||||||
() => SupabaseMockAuthService(),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AuthenticatorType.appflowyCloudSelfHost:
|
case AuthenticatorType.appflowyCloudSelfHost:
|
||||||
await useTestSelfHostedAppFlowyCloud();
|
await useTestSelfHostedAppFlowyCloud();
|
||||||
getIt.unregister<AuthService>();
|
getIt.unregister<AuthService>();
|
||||||
@ -242,13 +232,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> useTestSupabaseCloud() async {
|
|
||||||
await useSupabaseCloud(
|
|
||||||
url: TestEnv.supabaseUrl,
|
|
||||||
anonKey: TestEnv.supabaseAnonKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> useTestSelfHostedAppFlowyCloud() async {
|
Future<void> useTestSelfHostedAppFlowyCloud() async {
|
||||||
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
|
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ extension AppFlowySettings on WidgetTester {
|
|||||||
of: find.byType(UserProfileSetting),
|
of: find.byType(UserProfileSetting),
|
||||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||||
);
|
);
|
||||||
await tap(editUsernameFinder);
|
await tap(editUsernameFinder, warnIfMissed: false);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
|
|
||||||
final userNameFinder = find.descendant(
|
final userNameFinder = find.descendant(
|
||||||
|
@ -63,12 +63,15 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- printing (1.0.0):
|
|
||||||
- Flutter
|
|
||||||
- ReachabilitySwift (5.0.0)
|
- ReachabilitySwift (5.0.0)
|
||||||
- SDWebImage (5.14.2):
|
- SDWebImage (5.14.2):
|
||||||
- SDWebImage/Core (= 5.14.2)
|
- SDWebImage/Core (= 5.14.2)
|
||||||
- SDWebImage/Core (5.14.2)
|
- SDWebImage/Core (5.14.2)
|
||||||
|
- Sentry/HybridSDK (8.33.0)
|
||||||
|
- sentry_flutter (8.7.0):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- Sentry/HybridSDK (= 8.33.0)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@ -100,7 +103,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- printing (from `.symlinks/plugins/printing/ios`)
|
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
@ -113,6 +116,7 @@ SPEC REPOS:
|
|||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
|
- Sentry
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- Toast
|
- Toast
|
||||||
|
|
||||||
@ -147,8 +151,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
printing:
|
sentry_flutter:
|
||||||
:path: ".symlinks/plugins/printing/ios"
|
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@ -170,7 +174,7 @@ SPEC CHECKSUMS:
|
|||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
|
||||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||||
@ -178,9 +182,10 @@ SPEC CHECKSUMS:
|
|||||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||||
|
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
|
||||||
|
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
|
||||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
@ -191,4 +196,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.11.3
|
||||||
|
@ -13,7 +13,6 @@ class AppFlowyConfiguration {
|
|||||||
required this.device_id,
|
required this.device_id,
|
||||||
required this.platform,
|
required this.platform,
|
||||||
required this.authenticator_type,
|
required this.authenticator_type,
|
||||||
required this.supabase_config,
|
|
||||||
required this.appflowy_cloud_config,
|
required this.appflowy_cloud_config,
|
||||||
required this.envs,
|
required this.envs,
|
||||||
});
|
});
|
||||||
@ -28,41 +27,12 @@ class AppFlowyConfiguration {
|
|||||||
final String device_id;
|
final String device_id;
|
||||||
final String platform;
|
final String platform;
|
||||||
final int authenticator_type;
|
final int authenticator_type;
|
||||||
final SupabaseConfiguration supabase_config;
|
|
||||||
final AppFlowyCloudConfiguration appflowy_cloud_config;
|
final AppFlowyCloudConfiguration appflowy_cloud_config;
|
||||||
final Map<String, String> envs;
|
final Map<String, String> envs;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
|
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class SupabaseConfiguration {
|
|
||||||
SupabaseConfiguration({
|
|
||||||
required this.url,
|
|
||||||
required this.anon_key,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SupabaseConfiguration.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SupabaseConfigurationFromJson(json);
|
|
||||||
|
|
||||||
/// Indicates whether the sync feature is enabled.
|
|
||||||
final String url;
|
|
||||||
final String anon_key;
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
|
|
||||||
|
|
||||||
static SupabaseConfiguration defaultConfig() {
|
|
||||||
return SupabaseConfiguration(
|
|
||||||
url: '',
|
|
||||||
anon_key: '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isValid {
|
|
||||||
return url.isNotEmpty && anon_key.isNotEmpty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class AppFlowyCloudConfiguration {
|
class AppFlowyCloudConfiguration {
|
||||||
AppFlowyCloudConfiguration({
|
AppFlowyCloudConfiguration({
|
||||||
|
67
frontend/appflowy_flutter/lib/env/cloud_env.dart
vendored
@ -21,9 +21,6 @@ Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
|
|||||||
case AuthenticatorType.local:
|
case AuthenticatorType.local:
|
||||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
||||||
break;
|
break;
|
||||||
case AuthenticatorType.supabase:
|
|
||||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
|
|
||||||
break;
|
|
||||||
case AuthenticatorType.appflowyCloud:
|
case AuthenticatorType.appflowyCloud:
|
||||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
||||||
break;
|
break;
|
||||||
@ -63,8 +60,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
|||||||
switch (value ?? "0") {
|
switch (value ?? "0") {
|
||||||
case "0":
|
case "0":
|
||||||
return AuthenticatorType.local;
|
return AuthenticatorType.local;
|
||||||
case "1":
|
|
||||||
return AuthenticatorType.supabase;
|
|
||||||
case "2":
|
case "2":
|
||||||
return AuthenticatorType.appflowyCloud;
|
return AuthenticatorType.appflowyCloud;
|
||||||
case "3":
|
case "3":
|
||||||
@ -93,10 +88,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
|||||||
/// Returns `false` otherwise.
|
/// Returns `false` otherwise.
|
||||||
bool get isAuthEnabled {
|
bool get isAuthEnabled {
|
||||||
final env = getIt<AppFlowyCloudSharedEnv>();
|
final env = getIt<AppFlowyCloudSharedEnv>();
|
||||||
if (env.authenticatorType == AuthenticatorType.supabase) {
|
|
||||||
return env.supabaseConfig.isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.authenticatorType.isAppFlowyCloudEnabled) {
|
if (env.authenticatorType.isAppFlowyCloudEnabled) {
|
||||||
return env.appflowyCloudConfig.isValid;
|
return env.appflowyCloudConfig.isValid;
|
||||||
}
|
}
|
||||||
@ -104,19 +95,6 @@ bool get isAuthEnabled {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if Supabase is enabled.
|
|
||||||
///
|
|
||||||
/// This getter evaluates if Supabase should be enabled based on the
|
|
||||||
/// current integration mode and cloud type setting.
|
|
||||||
///
|
|
||||||
/// Returns:
|
|
||||||
/// A boolean value indicating whether Supabase is enabled. It returns `true`
|
|
||||||
/// if the application is in release or develop mode and the current cloud type
|
|
||||||
/// is `CloudType.supabase`. Otherwise, it returns `false`.
|
|
||||||
bool get isSupabaseEnabled {
|
|
||||||
return currentCloudType().isSupabaseEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines if AppFlowy Cloud is enabled.
|
/// Determines if AppFlowy Cloud is enabled.
|
||||||
bool get isAppFlowyCloudEnabled {
|
bool get isAppFlowyCloudEnabled {
|
||||||
return currentCloudType().isAppFlowyCloudEnabled;
|
return currentCloudType().isAppFlowyCloudEnabled;
|
||||||
@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled {
|
|||||||
|
|
||||||
enum AuthenticatorType {
|
enum AuthenticatorType {
|
||||||
local,
|
local,
|
||||||
supabase,
|
|
||||||
appflowyCloud,
|
appflowyCloud,
|
||||||
appflowyCloudSelfHost,
|
appflowyCloudSelfHost,
|
||||||
// The 'appflowyCloudDevelop' type is used for develop purposes only.
|
// The 'appflowyCloudDevelop' type is used for develop purposes only.
|
||||||
@ -137,14 +114,10 @@ enum AuthenticatorType {
|
|||||||
this == AuthenticatorType.appflowyCloudDevelop ||
|
this == AuthenticatorType.appflowyCloudDevelop ||
|
||||||
this == AuthenticatorType.appflowyCloud;
|
this == AuthenticatorType.appflowyCloud;
|
||||||
|
|
||||||
bool get isSupabaseEnabled => this == AuthenticatorType.supabase;
|
|
||||||
|
|
||||||
int get value {
|
int get value {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case AuthenticatorType.local:
|
case AuthenticatorType.local:
|
||||||
return 0;
|
return 0;
|
||||||
case AuthenticatorType.supabase:
|
|
||||||
return 1;
|
|
||||||
case AuthenticatorType.appflowyCloud:
|
case AuthenticatorType.appflowyCloud:
|
||||||
return 2;
|
return 2;
|
||||||
case AuthenticatorType.appflowyCloudSelfHost:
|
case AuthenticatorType.appflowyCloudSelfHost:
|
||||||
@ -158,8 +131,6 @@ enum AuthenticatorType {
|
|||||||
switch (value) {
|
switch (value) {
|
||||||
case 0:
|
case 0:
|
||||||
return AuthenticatorType.local;
|
return AuthenticatorType.local;
|
||||||
case 1:
|
|
||||||
return AuthenticatorType.supabase;
|
|
||||||
case 2:
|
case 2:
|
||||||
return AuthenticatorType.appflowyCloud;
|
return AuthenticatorType.appflowyCloud;
|
||||||
case 3:
|
case 3:
|
||||||
@ -197,25 +168,15 @@ Future<void> useLocalServer() async {
|
|||||||
await _setAuthenticatorType(AuthenticatorType.local);
|
await _setAuthenticatorType(AuthenticatorType.local);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> useSupabaseCloud({
|
|
||||||
required String url,
|
|
||||||
required String anonKey,
|
|
||||||
}) async {
|
|
||||||
await _setAuthenticatorType(AuthenticatorType.supabase);
|
|
||||||
await setSupabaseServer(url, anonKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
||||||
class AppFlowyCloudSharedEnv {
|
class AppFlowyCloudSharedEnv {
|
||||||
AppFlowyCloudSharedEnv({
|
AppFlowyCloudSharedEnv({
|
||||||
required AuthenticatorType authenticatorType,
|
required AuthenticatorType authenticatorType,
|
||||||
required this.appflowyCloudConfig,
|
required this.appflowyCloudConfig,
|
||||||
required this.supabaseConfig,
|
|
||||||
}) : _authenticatorType = authenticatorType;
|
}) : _authenticatorType = authenticatorType;
|
||||||
|
|
||||||
final AuthenticatorType _authenticatorType;
|
final AuthenticatorType _authenticatorType;
|
||||||
final AppFlowyCloudConfiguration appflowyCloudConfig;
|
final AppFlowyCloudConfiguration appflowyCloudConfig;
|
||||||
final SupabaseConfiguration supabaseConfig;
|
|
||||||
|
|
||||||
AuthenticatorType get authenticatorType => _authenticatorType;
|
AuthenticatorType get authenticatorType => _authenticatorType;
|
||||||
|
|
||||||
@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv {
|
|||||||
? await getAppFlowyCloudConfig(authenticatorType)
|
? await getAppFlowyCloudConfig(authenticatorType)
|
||||||
: AppFlowyCloudConfiguration.defaultConfig();
|
: AppFlowyCloudConfiguration.defaultConfig();
|
||||||
|
|
||||||
final supabaseCloudConfig = authenticatorType.isSupabaseEnabled
|
|
||||||
? await getSupabaseCloudConfig()
|
|
||||||
: SupabaseConfiguration.defaultConfig();
|
|
||||||
|
|
||||||
// In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,
|
// In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,
|
||||||
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
|
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
|
||||||
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
|
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
|
||||||
@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv {
|
|||||||
return AppFlowyCloudSharedEnv(
|
return AppFlowyCloudSharedEnv(
|
||||||
authenticatorType: authenticatorType,
|
authenticatorType: authenticatorType,
|
||||||
appflowyCloudConfig: appflowyCloudConfig,
|
appflowyCloudConfig: appflowyCloudConfig,
|
||||||
supabaseConfig: supabaseCloudConfig,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Using the cloud settings from the .env file.
|
// Using the cloud settings from the .env file.
|
||||||
@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv {
|
|||||||
return AppFlowyCloudSharedEnv(
|
return AppFlowyCloudSharedEnv(
|
||||||
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
|
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
|
||||||
appflowyCloudConfig: appflowyCloudConfig,
|
appflowyCloudConfig: appflowyCloudConfig,
|
||||||
supabaseConfig: SupabaseConfiguration.defaultConfig(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv {
|
|||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'authenticator: $_authenticatorType\n'
|
return 'authenticator: $_authenticatorType\n'
|
||||||
'appflowy: ${appflowyCloudConfig.toJson()}\n'
|
'appflowy: ${appflowyCloudConfig.toJson()}\n';
|
||||||
'supabase: ${supabaseConfig.toJson()})\n';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,22 +308,3 @@ Future<void> setSupabaseServer(
|
|||||||
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
|
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
|
|
||||||
final url = await _getSupabaseUrl();
|
|
||||||
final anonKey = await _getSupabaseAnonKey();
|
|
||||||
return SupabaseConfiguration(
|
|
||||||
url: url,
|
|
||||||
anon_key: anonKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _getSupabaseUrl() async {
|
|
||||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
|
|
||||||
return result ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _getSupabaseAnonKey() async {
|
|
||||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
|
|
||||||
return result ?? '';
|
|
||||||
}
|
|
||||||
|
7
frontend/appflowy_flutter/lib/env/env.dart
vendored
@ -36,4 +36,11 @@ abstract class Env {
|
|||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
)
|
)
|
||||||
static const String internalBuild = _Env.internalBuild;
|
static const String internalBuild = _Env.internalBuild;
|
||||||
|
|
||||||
|
@EnviedField(
|
||||||
|
obfuscate: false,
|
||||||
|
varName: 'SENTRY_DSN',
|
||||||
|
defaultValue: '',
|
||||||
|
)
|
||||||
|
static const String sentryDsn = _Env.sentryDsn;
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,41 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
|
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
extension MobileRouter on BuildContext {
|
extension MobileRouter on BuildContext {
|
||||||
Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async {
|
Future<void> pushView(
|
||||||
|
ViewPB view, {
|
||||||
|
Map<String, dynamic>? arguments,
|
||||||
|
bool addInRecent = true,
|
||||||
|
bool showMoreButton = true,
|
||||||
|
String? fixedTitle,
|
||||||
|
}) async {
|
||||||
// set the current view before pushing the new view
|
// set the current view before pushing the new view
|
||||||
getIt<MenuSharedState>().latestOpenView = view;
|
getIt<MenuSharedState>().latestOpenView = view;
|
||||||
unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));
|
unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));
|
||||||
|
final queryParameters = view.queryParameters(arguments);
|
||||||
|
|
||||||
|
if (view.layout == ViewLayoutPB.Document) {
|
||||||
|
queryParameters[MobileDocumentScreen.viewShowMoreButton] =
|
||||||
|
showMoreButton.toString();
|
||||||
|
if (fixedTitle != null) {
|
||||||
|
queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final uri = Uri(
|
final uri = Uri(
|
||||||
path: view.routeName,
|
path: view.routeName,
|
||||||
queryParameters: view.queryParameters(arguments),
|
queryParameters: queryParameters,
|
||||||
).toString();
|
).toString();
|
||||||
await push(uri);
|
await push(uri);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
|
||||||
import 'package:appflowy/plugins/document/application/document_listener.dart';
|
import 'package:appflowy/plugins/document/application/document_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
@ -113,7 +111,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _service = DocumentService();
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final DocumentListener _documentListener;
|
final DocumentListener _documentListener;
|
||||||
final ViewListener _viewListener;
|
final ViewListener _viewListener;
|
||||||
@ -124,16 +121,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
|||||||
|
|
||||||
// for the version under 0.5.5
|
// for the version under 0.5.5
|
||||||
Future<(CoverType, String?)> getCoverV1() async {
|
Future<(CoverType, String?)> getCoverV1() async {
|
||||||
final result = await _service.getDocument(documentId: view.id);
|
|
||||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
|
||||||
if (document != null) {
|
|
||||||
final coverType = CoverType.fromString(
|
|
||||||
document.root.attributes[DocumentHeaderBlockKeys.coverType],
|
|
||||||
);
|
|
||||||
final coverValue = document
|
|
||||||
.root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
|
|
||||||
return (coverType, coverValue);
|
|
||||||
}
|
|
||||||
return (CoverType.none, null);
|
return (CoverType.none, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
|||||||
UserProfileBloc() : super(const _Initial()) {
|
UserProfileBloc() : super(const _Initial()) {
|
||||||
on<UserProfileEvent>((event, emit) async {
|
on<UserProfileEvent>((event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
started: () async => _initalize(emit),
|
started: () async => _initialize(emit),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initalize(Emitter<UserProfileState> emit) async {
|
Future<void> _initialize(Emitter<UserProfileState> emit) async {
|
||||||
emit(const UserProfileState.loading());
|
emit(const UserProfileState.loading());
|
||||||
|
|
||||||
final workspaceOrFailure =
|
final workspaceOrFailure =
|
||||||
|
@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType {
|
|||||||
Widget getWidget(VoidCallback? onTap) {
|
Widget getWidget(VoidCallback? onTap) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case FlowyAppBarLeadingType.back:
|
case FlowyAppBarLeadingType.back:
|
||||||
return AppBarBackButton(onTap: onTap);
|
return AppBarImmersiveBackButton(onTap: onTap);
|
||||||
case FlowyAppBarLeadingType.close:
|
case FlowyAppBarLeadingType.close:
|
||||||
return AppBarCloseButton(onTap: onTap);
|
return AppBarCloseButton(onTap: onTap);
|
||||||
case FlowyAppBarLeadingType.cancel:
|
case FlowyAppBarLeadingType.cancel:
|
||||||
|
@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppBarImmersiveBackButton extends StatelessWidget {
|
||||||
|
const AppBarImmersiveBackButton({
|
||||||
|
super.key,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppBarButton(
|
||||||
|
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
right: 4.0,
|
||||||
|
),
|
||||||
|
child: const FlowySvg(
|
||||||
|
FlowySvgs.m_app_bar_back_s,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AppBarCloseButton extends StatelessWidget {
|
class AppBarCloseButton extends StatelessWidget {
|
||||||
const AppBarCloseButton({
|
const AppBarCloseButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
|||||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
@ -27,6 +27,8 @@ class MobileViewPage extends StatefulWidget {
|
|||||||
required this.viewLayout,
|
required this.viewLayout,
|
||||||
this.title,
|
this.title,
|
||||||
this.arguments,
|
this.arguments,
|
||||||
|
this.fixedTitle,
|
||||||
|
this.showMoreButton = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// view id
|
/// view id
|
||||||
@ -34,6 +36,10 @@ class MobileViewPage extends StatefulWidget {
|
|||||||
final ViewLayoutPB viewLayout;
|
final ViewLayoutPB viewLayout;
|
||||||
final String? title;
|
final String? title;
|
||||||
final Map<String, dynamic>? arguments;
|
final Map<String, dynamic>? arguments;
|
||||||
|
final bool showMoreButton;
|
||||||
|
|
||||||
|
// only used in row page
|
||||||
|
final String? fixedTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileViewPage> createState() => _MobileViewPageState();
|
State<MobileViewPage> createState() => _MobileViewPageState();
|
||||||
@ -164,6 +170,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||||||
return plugin.widgetBuilder.buildWidget(
|
return plugin.widgetBuilder.buildWidget(
|
||||||
shrinkWrap: false,
|
shrinkWrap: false,
|
||||||
context: PluginContext(userProfile: state.userProfilePB),
|
context: PluginContext(userProfile: state.userProfilePB),
|
||||||
|
data: {
|
||||||
|
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(error) {
|
(error) {
|
||||||
@ -216,13 +225,19 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.addAll([
|
if (widget.showMoreButton) {
|
||||||
MobileViewPageMoreButton(
|
actions.addAll([
|
||||||
view: view,
|
MobileViewPageMoreButton(
|
||||||
isImmersiveMode: isImmersiveMode,
|
view: view,
|
||||||
appBarOpacity: _appBarOpacity,
|
isImmersiveMode: isImmersiveMode,
|
||||||
),
|
appBarOpacity: _appBarOpacity,
|
||||||
]);
|
),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
actions.addAll([
|
||||||
|
const HSpace(18.0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
@ -232,19 +247,20 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (icon != null && icon.isNotEmpty)
|
if (icon != null && icon.isNotEmpty) ...[
|
||||||
ConstrainedBox(
|
FlowyText.emoji(
|
||||||
constraints: const BoxConstraints.tightFor(width: 34.0),
|
icon,
|
||||||
child: EmojiText(
|
fontSize: 15.0,
|
||||||
emoji: '$icon ',
|
figmaLineHeight: 18.0,
|
||||||
fontSize: 22.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
view?.name ?? widget.title ?? '',
|
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
|
||||||
fontSize: 15.0,
|
fontSize: 15.0,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
figmaLineHeight: 18.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState
|
|||||||
height: 42.0,
|
height: 42.0,
|
||||||
child: FlowyTextField(
|
child: FlowyTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||||
keyboardType: TextInputType.text,
|
keyboardType: TextInputType.text,
|
||||||
onSubmitted: (text) => widget.onRename(text),
|
onSubmitted: (text) => widget.onRename(text),
|
||||||
),
|
),
|
||||||
|
@ -116,12 +116,18 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
|||||||
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
|
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
|
||||||
await showFlowyCupertinoConfirmDialog(
|
await showFlowyCupertinoConfirmDialog(
|
||||||
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
|
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
|
||||||
leftButton: FlowyText.regular(
|
leftButton: FlowyText(
|
||||||
LocaleKeys.button_cancel.tr(),
|
LocaleKeys.button_cancel.tr(),
|
||||||
color: const Color(0xFF1456F0),
|
fontSize: 17.0,
|
||||||
|
figmaLineHeight: 24.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: const Color(0xFF007AFF),
|
||||||
),
|
),
|
||||||
rightButton: FlowyText.medium(
|
rightButton: FlowyText(
|
||||||
LocaleKeys.button_delete.tr(),
|
LocaleKeys.button_delete.tr(),
|
||||||
|
fontSize: 17.0,
|
||||||
|
figmaLineHeight: 24.0,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
color: const Color(0xFFFE0220),
|
color: const Color(0xFFFE0220),
|
||||||
),
|
),
|
||||||
onRightButtonPressed: (context) {
|
onRightButtonPressed: (context) {
|
||||||
|
@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
@ -294,6 +295,7 @@ class MobileRowDetailPageContentState
|
|||||||
RowCache get rowCache => widget.databaseController.rowCache;
|
RowCache get rowCache => widget.databaseController.rowCache;
|
||||||
FieldController get fieldController =>
|
FieldController get fieldController =>
|
||||||
widget.databaseController.fieldController;
|
widget.databaseController.fieldController;
|
||||||
|
ValueNotifier<String> primaryFieldId = ValueNotifier('');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -326,7 +328,13 @@ class MobileRowDetailPageContentState
|
|||||||
fieldController: fieldController,
|
fieldController: fieldController,
|
||||||
rowMeta: rowController.rowMeta,
|
rowMeta: rowController.rowMeta,
|
||||||
)..add(const RowBannerEvent.initial()),
|
)..add(const RowBannerEvent.initial()),
|
||||||
child: BlocBuilder<RowBannerBloc, RowBannerState>(
|
child: BlocConsumer<RowBannerBloc, RowBannerState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.primaryField == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
primaryFieldId.value = state.primaryField!.id;
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.primaryField == null) {
|
if (state.primaryField == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@ -366,6 +374,23 @@ class MobileRowDetailPageContentState
|
|||||||
if (rowDetailState.numHiddenFields != 0) ...[
|
if (rowDetailState.numHiddenFields != 0) ...[
|
||||||
const ToggleHiddenFieldsVisibilityButton(),
|
const ToggleHiddenFieldsVisibilityButton(),
|
||||||
],
|
],
|
||||||
|
const VSpace(8.0),
|
||||||
|
ValueListenableBuilder(
|
||||||
|
valueListenable: primaryFieldId,
|
||||||
|
builder: (context, primaryFieldId, child) {
|
||||||
|
if (primaryFieldId.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return OpenRowPageButton(
|
||||||
|
databaseController: widget.databaseController,
|
||||||
|
cellContext: CellContext(
|
||||||
|
rowId: rowController.rowId,
|
||||||
|
fieldId: primaryFieldId,
|
||||||
|
),
|
||||||
|
documentId: rowController.rowMeta.documentId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
MobileRowDetailCreateFieldButton(
|
MobileRowDetailCreateFieldButton(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fieldController: fieldController,
|
fieldController: fieldController,
|
||||||
|
@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
|
|||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minWidth: double.infinity,
|
minWidth: double.infinity,
|
||||||
minHeight: GridSize.headerHeight,
|
maxHeight: GridSize.headerHeight,
|
||||||
),
|
),
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
||||||
@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
|
|||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
splashFactory: NoSplash.splashFactory,
|
splashFactory: NoSplash.splashFactory,
|
||||||
padding: const WidgetStatePropertyAll(
|
padding: const WidgetStatePropertyAll(
|
||||||
EdgeInsets.symmetric(vertical: 14, horizontal: 6),
|
EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: FlowyText.medium(
|
label: FlowyText.medium(
|
||||||
|
@ -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,
|
super.key,
|
||||||
required this.id,
|
required this.id,
|
||||||
this.title,
|
this.title,
|
||||||
|
this.showMoreButton = true,
|
||||||
|
this.fixedTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// view id
|
/// view id
|
||||||
final String id;
|
final String id;
|
||||||
final String? title;
|
final String? title;
|
||||||
|
final bool showMoreButton;
|
||||||
|
final String? fixedTitle;
|
||||||
|
|
||||||
static const routeName = '/docs';
|
static const routeName = '/docs';
|
||||||
static const viewId = 'id';
|
static const viewId = 'id';
|
||||||
static const viewTitle = 'title';
|
static const viewTitle = 'title';
|
||||||
|
static const viewShowMoreButton = 'show_more_button';
|
||||||
|
static const viewFixedTitle = 'fixed_title';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget {
|
|||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
viewLayout: ViewLayoutPB.Document,
|
viewLayout: ViewLayoutPB.Document,
|
||||||
|
showMoreButton: showMoreButton,
|
||||||
|
fixedTitle: fixedTitle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,36 +96,34 @@ class _FavoriteViews extends StatelessWidget {
|
|||||||
final borderColor = Theme.of(context).isLightMode
|
final borderColor = Theme.of(context).isLightMode
|
||||||
? const Color(0xFFE9E9EC)
|
? const Color(0xFFE9E9EC)
|
||||||
: const Color(0x1AFFFFFF);
|
: const Color(0x1AFFFFFF);
|
||||||
return Scrollbar(
|
return ListView.separated(
|
||||||
child: ListView.separated(
|
key: const PageStorageKey('favorite_views_page_storage_key'),
|
||||||
key: const PageStorageKey('favorite_views_page_storage_key'),
|
padding: EdgeInsets.only(
|
||||||
padding: EdgeInsets.only(
|
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
MediaQuery.of(context).padding.bottom,
|
||||||
MediaQuery.of(context).padding.bottom,
|
),
|
||||||
),
|
itemBuilder: (context, index) {
|
||||||
itemBuilder: (context, index) {
|
final view = favoriteViews[index];
|
||||||
final view = favoriteViews[index];
|
return Container(
|
||||||
return Container(
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
border: Border(
|
||||||
border: Border(
|
bottom: BorderSide(
|
||||||
bottom: BorderSide(
|
color: borderColor,
|
||||||
color: borderColor,
|
width: 0.5,
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: MobileViewPage(
|
),
|
||||||
key: ValueKey(view.item.id),
|
child: MobileViewPage(
|
||||||
view: view.item,
|
key: ValueKey(view.item.id),
|
||||||
timestamp: view.timestamp,
|
view: view.item,
|
||||||
type: MobilePageCardType.favorite,
|
timestamp: view.timestamp,
|
||||||
),
|
type: MobilePageCardType.favorite,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
separatorBuilder: (context, index) => const HSpace(8),
|
},
|
||||||
itemCount: favoriteViews.length,
|
separatorBuilder: (context, index) => const HSpace(8),
|
||||||
),
|
itemCount: favoriteViews.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,19 +25,17 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
|||||||
final workspaceId =
|
final workspaceId =
|
||||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
||||||
'';
|
'';
|
||||||
return Scrollbar(
|
return SingleChildScrollView(
|
||||||
child: SingleChildScrollView(
|
child: Padding(
|
||||||
child: Padding(
|
padding: EdgeInsets.only(
|
||||||
padding: EdgeInsets.only(
|
top: HomeSpaceViewSizes.mVerticalPadding,
|
||||||
top: HomeSpaceViewSizes.mVerticalPadding,
|
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
MediaQuery.of(context).padding.bottom,
|
||||||
MediaQuery.of(context).padding.bottom,
|
),
|
||||||
),
|
child: MobileFolders(
|
||||||
child: MobileFolders(
|
user: widget.userProfile,
|
||||||
user: widget.userProfile,
|
workspaceId: workspaceId,
|
||||||
workspaceId: workspaceId,
|
showFavorite: false,
|
||||||
showFavorite: false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -35,6 +35,9 @@ class MobileFolders extends StatelessWidget {
|
|||||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
||||||
'';
|
'';
|
||||||
return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.currentWorkspace?.workspaceId !=
|
||||||
|
current.currentWorkspace?.workspaceId,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
context.read<SidebarSectionsBloc>().add(
|
context.read<SidebarSectionsBloc>().add(
|
||||||
SidebarSectionsEvent.initial(
|
SidebarSectionsEvent.initial(
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
@ -14,14 +16,19 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:sentry/sentry.dart';
|
||||||
|
import 'package:toastification/toastification.dart';
|
||||||
|
|
||||||
class MobileHomeScreen extends StatelessWidget {
|
class MobileHomeScreen extends StatelessWidget {
|
||||||
const MobileHomeScreen({super.key});
|
const MobileHomeScreen({super.key});
|
||||||
@ -59,6 +66,14 @@ class MobileHomeScreen extends StatelessWidget {
|
|||||||
return const WorkspaceFailedScreen();
|
return const WorkspaceFailedScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sentry.configureScope(
|
||||||
|
(scope) => scope.setUser(
|
||||||
|
SentryUser(
|
||||||
|
id: userProfile.id.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
@ -94,6 +109,8 @@ class MobileHomePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MobileHomePageState extends State<MobileHomePage> {
|
class _MobileHomePageState extends State<MobileHomePage> {
|
||||||
|
Loading? loadingIndicator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -125,75 +142,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
|||||||
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
child: _HomePage(userProfile: widget.userProfile),
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
previous.currentWorkspace?.workspaceId !=
|
|
||||||
current.currentWorkspace?.workspaceId,
|
|
||||||
listener: (context, state) {
|
|
||||||
getIt<CachedRecentService>().reset();
|
|
||||||
mCurrentWorkspace.value = state.currentWorkspace;
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.currentWorkspace == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final workspaceId = state.currentWorkspace!.workspaceId;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
// Header
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
|
||||||
right: 8.0,
|
|
||||||
top: Platform.isAndroid ? 8.0 : 0.0,
|
|
||||||
),
|
|
||||||
child: MobileHomePageHeader(
|
|
||||||
userProfile: widget.userProfile,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Expanded(
|
|
||||||
child: MultiBlocProvider(
|
|
||||||
providers: [
|
|
||||||
BlocProvider(
|
|
||||||
create: (_) => SpaceOrderBloc()
|
|
||||||
..add(const SpaceOrderEvent.initial()),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
create: (_) => SidebarSectionsBloc()
|
|
||||||
..add(
|
|
||||||
SidebarSectionsEvent.initial(
|
|
||||||
widget.userProfile,
|
|
||||||
workspaceId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
create: (_) =>
|
|
||||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
|
||||||
),
|
|
||||||
BlocProvider(
|
|
||||||
create: (_) => SpaceBloc()
|
|
||||||
..add(
|
|
||||||
SpaceEvent.initial(
|
|
||||||
widget.userProfile,
|
|
||||||
workspaceId,
|
|
||||||
openFirstPage: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: MobileSpaceTab(
|
|
||||||
userProfile: widget.userProfile,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,3 +154,147 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
|||||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HomePage extends StatefulWidget {
|
||||||
|
const _HomePage({required this.userProfile});
|
||||||
|
|
||||||
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<_HomePage> {
|
||||||
|
Loading? loadingIndicator;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.currentWorkspace?.workspaceId !=
|
||||||
|
current.currentWorkspace?.workspaceId,
|
||||||
|
listener: (context, state) {
|
||||||
|
getIt<CachedRecentService>().reset();
|
||||||
|
mCurrentWorkspace.value = state.currentWorkspace;
|
||||||
|
|
||||||
|
_showResultDialog(context, state);
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.currentWorkspace == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final workspaceId = state.currentWorkspace!.workspaceId;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
key: ValueKey('mobile_home_page_$workspaceId'),
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||||
|
right: 8.0,
|
||||||
|
top: Platform.isAndroid ? 8.0 : 0.0,
|
||||||
|
),
|
||||||
|
child: MobileHomePageHeader(
|
||||||
|
userProfile: widget.userProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) => SidebarSectionsBloc()
|
||||||
|
..add(
|
||||||
|
SidebarSectionsEvent.initial(
|
||||||
|
widget.userProfile,
|
||||||
|
workspaceId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) =>
|
||||||
|
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (_) => SpaceBloc()
|
||||||
|
..add(
|
||||||
|
SpaceEvent.initial(
|
||||||
|
widget.userProfile,
|
||||||
|
workspaceId,
|
||||||
|
openFirstPage: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MobileSpaceTab(
|
||||||
|
userProfile: widget.userProfile,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showResultDialog(BuildContext context, UserWorkspaceState state) {
|
||||||
|
final actionResult = state.actionResult;
|
||||||
|
if (actionResult == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final actionType = actionResult.actionType;
|
||||||
|
final result = actionResult.result;
|
||||||
|
final isLoading = actionResult.isLoading;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
loadingIndicator ??= Loading(context)..start();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
loadingIndicator?.stop();
|
||||||
|
loadingIndicator = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.onFailure((f) {
|
||||||
|
Log.error(
|
||||||
|
'[Workspace] Failed to perform ${actionType.toString()} action: $f',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? message;
|
||||||
|
ToastificationType toastType = ToastificationType.success;
|
||||||
|
switch (actionType) {
|
||||||
|
case UserWorkspaceActionType.open:
|
||||||
|
message = result.fold(
|
||||||
|
(s) {
|
||||||
|
toastType = ToastificationType.success;
|
||||||
|
return LocaleKeys.workspace_openSuccess.tr();
|
||||||
|
},
|
||||||
|
(e) {
|
||||||
|
toastType = ToastificationType.error;
|
||||||
|
return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
message = null;
|
||||||
|
toastType = ToastificationType.error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
showToastNotification(context, message: message, type: toastType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
|||||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
|
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
|
||||||
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
|
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
@ -79,8 +80,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
|||||||
PersonalInfoSettingGroup(
|
PersonalInfoSettingGroup(
|
||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
),
|
),
|
||||||
// TODO: Enable and implement along with Push Notifications
|
const WorkspaceSettingGroup(),
|
||||||
// const NotificationsSettingGroup(),
|
|
||||||
const AppearanceSettingGroup(),
|
const AppearanceSettingGroup(),
|
||||||
const LanguageSettingGroup(),
|
const LanguageSettingGroup(),
|
||||||
if (Env.enableCustomCloud) const CloudSettingGroup(),
|
if (Env.enableCustomCloud) const CloudSettingGroup(),
|
||||||
|
@ -68,36 +68,34 @@ class _RecentViews extends StatelessWidget {
|
|||||||
? const Color(0xFFE9E9EC)
|
? const Color(0xFFE9E9EC)
|
||||||
: const Color(0x1AFFFFFF);
|
: const Color(0x1AFFFFFF);
|
||||||
return SlidableAutoCloseBehavior(
|
return SlidableAutoCloseBehavior(
|
||||||
child: Scrollbar(
|
child: ListView.separated(
|
||||||
child: ListView.separated(
|
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
padding: EdgeInsets.only(
|
||||||
padding: EdgeInsets.only(
|
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
MediaQuery.of(context).padding.bottom,
|
||||||
MediaQuery.of(context).padding.bottom,
|
),
|
||||||
),
|
itemBuilder: (context, index) {
|
||||||
itemBuilder: (context, index) {
|
final sectionView = recentViews[index];
|
||||||
final sectionView = recentViews[index];
|
return Container(
|
||||||
return Container(
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
border: Border(
|
||||||
border: Border(
|
bottom: BorderSide(
|
||||||
bottom: BorderSide(
|
color: borderColor,
|
||||||
color: borderColor,
|
width: 0.5,
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: MobileViewPage(
|
),
|
||||||
key: ValueKey(sectionView.item.id),
|
child: MobileViewPage(
|
||||||
view: sectionView.item,
|
key: ValueKey(sectionView.item.id),
|
||||||
timestamp: sectionView.timestamp,
|
view: sectionView.item,
|
||||||
type: MobilePageCardType.recent,
|
timestamp: sectionView.timestamp,
|
||||||
),
|
type: MobilePageCardType.recent,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
separatorBuilder: (context, index) => const HSpace(8),
|
},
|
||||||
itemCount: recentViews.length,
|
separatorBuilder: (context, index) => const HSpace(8),
|
||||||
),
|
itemCount: recentViews.length,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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/_tab_bar.dart';
|
||||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -17,6 +20,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'ai_bubble_button.dart';
|
||||||
|
|
||||||
|
final ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
class MobileSpaceTab extends StatefulWidget {
|
class MobileSpaceTab extends StatefulWidget {
|
||||||
const MobileSpaceTab({
|
const MobileSpaceTab({
|
||||||
super.key,
|
super.key,
|
||||||
@ -37,14 +44,19 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
mobileCreateNewPageNotifier.addListener(_createNewPage);
|
mobileCreateNewPageNotifier.addListener(_createNewDocument);
|
||||||
|
mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);
|
||||||
|
mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
tabController?.removeListener(_onTabChange);
|
tabController?.removeListener(_onTabChange);
|
||||||
tabController?.dispose();
|
tabController?.dispose();
|
||||||
mobileCreateNewPageNotifier.removeListener(_createNewPage);
|
|
||||||
|
mobileCreateNewPageNotifier.removeListener(_createNewDocument);
|
||||||
|
mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);
|
||||||
|
mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -140,7 +152,20 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
|||||||
case MobileSpaceTabType.recent:
|
case MobileSpaceTabType.recent:
|
||||||
return const MobileRecentSpace();
|
return const MobileRecentSpace();
|
||||||
case MobileSpaceTabType.spaces:
|
case MobileSpaceTabType.spaces:
|
||||||
return MobileHomeSpace(userProfile: widget.userProfile);
|
return Stack(
|
||||||
|
children: [
|
||||||
|
MobileHomeSpace(userProfile: widget.userProfile),
|
||||||
|
// only show ai chat button for cloud user
|
||||||
|
if (widget.userProfile.authenticator ==
|
||||||
|
AuthenticatorPB.AppFlowyCloud)
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
child: const FloatingAIEntry(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
case MobileSpaceTabType.favorites:
|
case MobileSpaceTabType.favorites:
|
||||||
return MobileFavoriteSpace(userProfile: widget.userProfile);
|
return MobileFavoriteSpace(userProfile: widget.userProfile);
|
||||||
default:
|
default:
|
||||||
@ -150,15 +175,24 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// quick create new page when clicking the add button in navigation bar
|
// quick create new page when clicking the add button in navigation bar
|
||||||
void _createNewPage() {
|
void _createNewDocument() {
|
||||||
|
_createNewPage(ViewLayoutPB.Document);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createNewAIChat() {
|
||||||
|
_createNewPage(ViewLayoutPB.Chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createNewPage(ViewLayoutPB layout) {
|
||||||
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
|
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
|
||||||
context.read<SpaceBloc>().add(
|
context.read<SpaceBloc>().add(
|
||||||
SpaceEvent.createPage(
|
SpaceEvent.createPage(
|
||||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
layout: ViewLayoutPB.Document,
|
layout: layout,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else if (layout == ViewLayoutPB.Document) {
|
||||||
|
// only support create document in section
|
||||||
context.read<SidebarSectionsBloc>().add(
|
context.read<SidebarSectionsBloc>().add(
|
||||||
SidebarSectionsEvent.createRootViewInSection(
|
SidebarSectionsEvent.createRootViewInSection(
|
||||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
@ -171,4 +205,16 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _leaveWorkspace() {
|
||||||
|
final workspaceId =
|
||||||
|
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;
|
||||||
|
if (workspaceId == null) {
|
||||||
|
Log.error('Workspace ID is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context
|
||||||
|
.read<UserWorkspaceBloc>()
|
||||||
|
.add(UserWorkspaceEvent.leaveWorkspace(workspaceId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
|||||||
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/share_log_files.dart';
|
import 'package:appflowy/util/share_log_files.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -74,10 +75,14 @@ class SupportSettingGroup extends StatelessWidget {
|
|||||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||||
onActionButtonPressed: () async {
|
onActionButtonPressed: () async {
|
||||||
await getIt<FlowyCacheManager>().clearAllCache();
|
await getIt<FlowyCacheManager>().clearAllCache();
|
||||||
|
// check the workspace and space health
|
||||||
|
await WorkspaceDataManager.checkViewHealth(
|
||||||
|
dryRun: false,
|
||||||
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showSnackBarMessage(
|
showToastNotification(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -42,14 +42,24 @@ class UserSessionSettingGroup extends StatelessWidget {
|
|||||||
MobileSignInOrLogoutButton(
|
MobileSignInOrLogoutButton(
|
||||||
labelText: LocaleKeys.settings_menu_logout.tr(),
|
labelText: LocaleKeys.settings_menu_logout.tr(),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await showFlowyMobileConfirmDialog(
|
await showFlowyCupertinoConfirmDialog(
|
||||||
context,
|
title: LocaleKeys.settings_menu_logoutPrompt.tr(),
|
||||||
content: FlowyText(
|
leftButton: FlowyText(
|
||||||
LocaleKeys.settings_menu_logoutPrompt.tr(),
|
LocaleKeys.button_cancel.tr(),
|
||||||
|
fontSize: 17.0,
|
||||||
|
figmaLineHeight: 24.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: const Color(0xFF007AFF),
|
||||||
),
|
),
|
||||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
rightButton: FlowyText(
|
||||||
actionButtonColor: Theme.of(context).colorScheme.error,
|
LocaleKeys.button_logout.tr(),
|
||||||
onActionButtonPressed: () async {
|
fontSize: 17.0,
|
||||||
|
figmaLineHeight: 24.0,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: const Color(0xFFFE0220),
|
||||||
|
),
|
||||||
|
onRightButtonPressed: (context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
await getIt<AuthService>().signOut();
|
await getIt<AuthService>().signOut();
|
||||||
await runAppFlowy();
|
await runAppFlowy();
|
||||||
},
|
},
|
||||||
|
@ -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(
|
return showDialog(
|
||||||
context: context ?? AppGlobals.context,
|
context: context ?? AppGlobals.context,
|
||||||
|
barrierColor: Colors.black.withOpacity(0.25),
|
||||||
builder: (context) => CupertinoAlertDialog(
|
builder: (context) => CupertinoAlertDialog(
|
||||||
title: FlowyText.medium(
|
title: FlowyText.medium(
|
||||||
title,
|
title,
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
lineHeight: 1.3,
|
figmaLineHeight: 22.0,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
|
@ -425,13 +425,14 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
Opacity(
|
if (PlatformExtension.isDesktop)
|
||||||
opacity: 0.6,
|
Opacity(
|
||||||
child: FlowyText(
|
opacity: 0.6,
|
||||||
LocaleKeys.chat_aiMistakePrompt.tr(),
|
child: FlowyText(
|
||||||
fontSize: 12,
|
LocaleKeys.chat_aiMistakePrompt.tr(),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -73,6 +73,7 @@ class ChatWelcomePage extends StatelessWidget {
|
|||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
Wrap(
|
Wrap(
|
||||||
direction: Axis.vertical,
|
direction: Axis.vertical,
|
||||||
|
spacing: isMobile ? 12.0 : 0.0,
|
||||||
children: items
|
children: items
|
||||||
.map(
|
.map(
|
||||||
(i) => WelcomeQuestionWidget(
|
(i) => WelcomeQuestionWidget(
|
||||||
|
@ -23,9 +23,9 @@ class RelatedRowDetailPageBloc
|
|||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
state.whenOrNull(
|
state.whenOrNull(
|
||||||
ready: (databaseController, rowController) {
|
ready: (databaseController, rowController) async {
|
||||||
rowController.dispose();
|
await rowController.dispose();
|
||||||
databaseController.dispose();
|
await databaseController.dispose();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return super.close();
|
return super.close();
|
||||||
@ -36,8 +36,8 @@ class RelatedRowDetailPageBloc
|
|||||||
event.when(
|
event.when(
|
||||||
didInitialize: (databaseController, rowController) {
|
didInitialize: (databaseController, rowController) {
|
||||||
state.maybeWhen(
|
state.maybeWhen(
|
||||||
ready: (_, oldRowController) {
|
ready: (_, oldRowController) async {
|
||||||
oldRowController.dispose();
|
await oldRowController.dispose();
|
||||||
emit(
|
emit(
|
||||||
RelatedRowDetailPageState.ready(
|
RelatedRowDetailPageState.ready(
|
||||||
databaseController: databaseController,
|
databaseController: databaseController,
|
||||||
|
@ -81,6 +81,12 @@ class RowCache {
|
|||||||
_changedNotifier.receive(const ChangedReason.setInitialRows());
|
_changedNotifier.receive(const ChangedReason.setInitialRows());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRowMeta(RowMetaPB rowMeta) {
|
||||||
|
final rowInfo = buildGridRow(rowMeta);
|
||||||
|
_rowList.add(rowInfo);
|
||||||
|
_changedNotifier.receive(const ChangedReason.didFetchRow());
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_rowLifeCycle.onRowDisposed();
|
_rowLifeCycle.onRowDisposed();
|
||||||
_changedNotifier.dispose();
|
_changedNotifier.dispose();
|
||||||
@ -215,7 +221,8 @@ class RowCache {
|
|||||||
if (rowInfo == null) {
|
if (rowInfo == null) {
|
||||||
_loadRow(rowMeta.id);
|
_loadRow(rowMeta.id);
|
||||||
}
|
}
|
||||||
return _makeCells(rowMeta);
|
final cells = _makeCells(rowMeta);
|
||||||
|
return cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadRow(RowId rowId) async {
|
Future<void> _loadRow(RowId rowId) async {
|
||||||
@ -277,6 +284,7 @@ class RowChangesetNotifier extends ChangeNotifier {
|
|||||||
reorderRows: (_) => notifyListeners(),
|
reorderRows: (_) => notifyListeners(),
|
||||||
reorderSingleRow: (_) => notifyListeners(),
|
reorderSingleRow: (_) => notifyListeners(),
|
||||||
setInitialRows: (_) => notifyListeners(),
|
setInitialRows: (_) => notifyListeners(),
|
||||||
|
didFetchRow: (_) => notifyListeners(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,6 +313,7 @@ class ChangedReason with _$ChangedReason {
|
|||||||
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
|
const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;
|
||||||
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
|
const factory ChangedReason.fieldDidChange() = _FieldDidChange;
|
||||||
const factory ChangedReason.initial() = InitialListState;
|
const factory ChangedReason.initial() = InitialListState;
|
||||||
|
const factory ChangedReason.didFetchRow() = _DidFetchRow;
|
||||||
const factory ChangedReason.reorderRows() = _ReorderRows;
|
const factory ChangedReason.reorderRows() = _ReorderRows;
|
||||||
const factory ChangedReason.reorderSingleRow(
|
const factory ChangedReason.reorderSingleRow(
|
||||||
ReorderSingleRowPB reorderRow,
|
ReorderSingleRowPB reorderRow,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database/domain/row_listener.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -9,35 +11,60 @@ typedef OnRowChanged = void Function(List<CellContext>, ChangedReason);
|
|||||||
|
|
||||||
class RowController {
|
class RowController {
|
||||||
RowController({
|
RowController({
|
||||||
required this.rowMeta,
|
required RowMetaPB rowMeta,
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required RowCache rowCache,
|
required RowCache rowCache,
|
||||||
this.groupId,
|
this.groupId,
|
||||||
}) : _rowCache = rowCache;
|
}) : _rowMeta = rowMeta,
|
||||||
|
_rowCache = rowCache,
|
||||||
|
_rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
|
_rowListener = RowListener(rowMeta.id) {
|
||||||
|
_rowBackendSvc.initRow(rowMeta.id);
|
||||||
|
_rowListener.start(
|
||||||
|
onMetaChanged: (newRowMeta) {
|
||||||
|
if (_isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_rowMeta = newRowMeta;
|
||||||
|
_rowCache.setRowMeta(newRowMeta);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final RowMetaPB rowMeta;
|
RowMetaPB _rowMeta;
|
||||||
final String? groupId;
|
final String? groupId;
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final List<VoidCallback> _onRowChangedListeners = [];
|
final List<VoidCallback> _onRowChangedListeners = [];
|
||||||
final RowCache _rowCache;
|
final RowCache _rowCache;
|
||||||
|
final RowListener _rowListener;
|
||||||
|
final RowBackendService _rowBackendSvc;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
CellMemCache get cellCache => _rowCache.cellCache;
|
CellMemCache get cellCache => _rowCache.cellCache;
|
||||||
|
|
||||||
String get rowId => rowMeta.id;
|
String get rowId => rowMeta.id;
|
||||||
|
RowMetaPB get rowMeta => _rowMeta;
|
||||||
|
|
||||||
List<CellContext> loadData() => _rowCache.loadCells(rowMeta);
|
List<CellContext> loadCells() => _rowCache.loadCells(rowMeta);
|
||||||
|
|
||||||
void addListener({OnRowChanged? onRowChanged}) {
|
void addListener({OnRowChanged? onRowChanged}) {
|
||||||
final fn = _rowCache.addListener(
|
final fn = _rowCache.addListener(
|
||||||
rowId: rowMeta.id,
|
rowId: rowMeta.id,
|
||||||
onRowChanged: onRowChanged,
|
onRowChanged: (context, reasons) {
|
||||||
|
if (_isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onRowChanged?.call(context, reasons);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add the listener to the list so that we can remove it later.
|
// Add the listener to the list so that we can remove it later.
|
||||||
_onRowChangedListeners.add(fn);
|
_onRowChangedListeners.add(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
Future<void> dispose() async {
|
||||||
|
_isDisposed = true;
|
||||||
|
await _rowListener.stop();
|
||||||
for (final fn in _onRowChangedListeners) {
|
for (final fn in _onRowChangedListeners) {
|
||||||
_rowCache.removeRowListener(fn);
|
_rowCache.removeRowListener(fn);
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,14 @@ class RowBackendService {
|
|||||||
return DatabaseEventCreateRow(payload).send();
|
return DatabaseEventCreateRow(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<FlowyResult<void, FlowyError>> initRow(RowId rowId) async {
|
||||||
|
final payload = RowIdPB()
|
||||||
|
..viewId = viewId
|
||||||
|
..rowId = rowId;
|
||||||
|
|
||||||
|
return DatabaseEventInitRow(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
Future<FlowyResult<RowMetaPB, FlowyError>> createRowBefore(RowId rowId) {
|
Future<FlowyResult<RowMetaPB, FlowyError>> createRowBefore(RowId rowId) {
|
||||||
return createRow(
|
return createRow(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
|
@ -2,7 +2,9 @@ import 'dart:async';
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
|
||||||
import '../defines.dart';
|
import '../defines.dart';
|
||||||
import '../field/field_controller.dart';
|
import '../field/field_controller.dart';
|
||||||
@ -91,6 +93,17 @@ class DatabaseViewCache {
|
|||||||
(reorderRow) => _rowCache.reorderSingleRow(reorderRow),
|
(reorderRow) => _rowCache.reorderSingleRow(reorderRow),
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
),
|
),
|
||||||
|
onReloadRows: () {
|
||||||
|
final payload = DatabaseViewIdPB(value: viewId);
|
||||||
|
DatabaseEventGetAllRows(payload).send().then((result) {
|
||||||
|
result.fold(
|
||||||
|
(rows) {
|
||||||
|
_rowCache.setInitialRows(rows.items);
|
||||||
|
},
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_rowCache.onRowsChanged(
|
_rowCache.onRowsChanged(
|
||||||
|
@ -7,85 +7,96 @@ import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
|
|
||||||
typedef RowsVisibilityNotifierValue
|
typedef RowsVisibilityCallback = void Function(
|
||||||
= FlowyResult<RowsVisibilityChangePB, FlowyError>;
|
FlowyResult<RowsVisibilityChangePB, FlowyError>,
|
||||||
|
);
|
||||||
typedef NumberOfRowsNotifierValue = FlowyResult<RowsChangePB, FlowyError>;
|
typedef NumberOfRowsCallback = void Function(
|
||||||
typedef ReorderAllRowsNotifierValue = FlowyResult<List<String>, FlowyError>;
|
FlowyResult<RowsChangePB, FlowyError>,
|
||||||
typedef SingleRowNotifierValue = FlowyResult<ReorderSingleRowPB, FlowyError>;
|
);
|
||||||
|
typedef ReorderAllRowsCallback = void Function(
|
||||||
|
FlowyResult<List<String>, FlowyError>,
|
||||||
|
);
|
||||||
|
typedef SingleRowCallback = void Function(
|
||||||
|
FlowyResult<ReorderSingleRowPB, FlowyError>,
|
||||||
|
);
|
||||||
|
|
||||||
class DatabaseViewListener {
|
class DatabaseViewListener {
|
||||||
DatabaseViewListener({required this.viewId});
|
DatabaseViewListener({required this.viewId});
|
||||||
|
|
||||||
final String viewId;
|
final String viewId;
|
||||||
|
|
||||||
PublishNotifier<NumberOfRowsNotifierValue>? _rowsNotifier = PublishNotifier();
|
|
||||||
PublishNotifier<ReorderAllRowsNotifierValue>? _reorderAllRows =
|
|
||||||
PublishNotifier();
|
|
||||||
PublishNotifier<SingleRowNotifierValue>? _reorderSingleRow =
|
|
||||||
PublishNotifier();
|
|
||||||
PublishNotifier<RowsVisibilityNotifierValue>? _rowsVisibility =
|
|
||||||
PublishNotifier();
|
|
||||||
|
|
||||||
DatabaseNotificationListener? _listener;
|
DatabaseNotificationListener? _listener;
|
||||||
|
|
||||||
void start({
|
void start({
|
||||||
required void Function(NumberOfRowsNotifierValue) onRowsChanged,
|
required NumberOfRowsCallback onRowsChanged,
|
||||||
required void Function(ReorderAllRowsNotifierValue) onReorderAllRows,
|
required ReorderAllRowsCallback onReorderAllRows,
|
||||||
required void Function(SingleRowNotifierValue) onReorderSingleRow,
|
required SingleRowCallback onReorderSingleRow,
|
||||||
required void Function(RowsVisibilityNotifierValue) onRowsVisibilityChanged,
|
required RowsVisibilityCallback onRowsVisibilityChanged,
|
||||||
|
required void Function() onReloadRows,
|
||||||
}) {
|
}) {
|
||||||
if (_listener != null) {
|
// Stop any existing listener
|
||||||
_listener?.stop();
|
_listener?.stop();
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initialize the notification listener
|
||||||
_listener = DatabaseNotificationListener(
|
_listener = DatabaseNotificationListener(
|
||||||
objectId: viewId,
|
objectId: viewId,
|
||||||
handler: _handler,
|
handler: (ty, result) => _handler(
|
||||||
|
ty,
|
||||||
|
result,
|
||||||
|
onRowsChanged,
|
||||||
|
onReorderAllRows,
|
||||||
|
onReorderSingleRow,
|
||||||
|
onRowsVisibilityChanged,
|
||||||
|
onReloadRows,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_rowsNotifier?.addPublishListener(onRowsChanged);
|
|
||||||
_rowsVisibility?.addPublishListener(onRowsVisibilityChanged);
|
|
||||||
_reorderAllRows?.addPublishListener(onReorderAllRows);
|
|
||||||
_reorderSingleRow?.addPublishListener(onReorderSingleRow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handler(
|
void _handler(
|
||||||
DatabaseNotification ty,
|
DatabaseNotification ty,
|
||||||
FlowyResult<Uint8List, FlowyError> result,
|
FlowyResult<Uint8List, FlowyError> result,
|
||||||
|
NumberOfRowsCallback onRowsChanged,
|
||||||
|
ReorderAllRowsCallback onReorderAllRows,
|
||||||
|
SingleRowCallback onReorderSingleRow,
|
||||||
|
RowsVisibilityCallback onRowsVisibilityChanged,
|
||||||
|
void Function() onReloadRows,
|
||||||
) {
|
) {
|
||||||
switch (ty) {
|
switch (ty) {
|
||||||
case DatabaseNotification.DidUpdateViewRowsVisibility:
|
case DatabaseNotification.DidUpdateViewRowsVisibility:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) => _rowsVisibility?.value =
|
(payload) => onRowsVisibilityChanged(
|
||||||
FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)),
|
FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)),
|
||||||
(error) => _rowsVisibility?.value = FlowyResult.failure(error),
|
),
|
||||||
|
(error) => onRowsVisibilityChanged(FlowyResult.failure(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case DatabaseNotification.DidUpdateRow:
|
case DatabaseNotification.DidUpdateRow:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) => _rowsNotifier?.value =
|
(payload) => onRowsChanged(
|
||||||
FlowyResult.success(RowsChangePB.fromBuffer(payload)),
|
FlowyResult.success(RowsChangePB.fromBuffer(payload)),
|
||||||
(error) => _rowsNotifier?.value = FlowyResult.failure(error),
|
),
|
||||||
|
(error) => onRowsChanged(FlowyResult.failure(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case DatabaseNotification.DidReorderRows:
|
case DatabaseNotification.DidReorderRows:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) => _reorderAllRows?.value = FlowyResult.success(
|
(payload) => onReorderAllRows(
|
||||||
ReorderAllRowsPB.fromBuffer(payload).rowOrders,
|
FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders),
|
||||||
),
|
),
|
||||||
(error) => _reorderAllRows?.value = FlowyResult.failure(error),
|
(error) => onReorderAllRows(FlowyResult.failure(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case DatabaseNotification.DidReorderSingleRow:
|
case DatabaseNotification.DidReorderSingleRow:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) => _reorderSingleRow?.value =
|
(payload) => onReorderSingleRow(
|
||||||
FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)),
|
FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)),
|
||||||
(error) => _reorderSingleRow?.value = FlowyResult.failure(error),
|
),
|
||||||
|
(error) => onReorderSingleRow(FlowyResult.failure(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case DatabaseNotification.ReloadRows:
|
||||||
|
onReloadRows();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -93,16 +104,6 @@ class DatabaseViewListener {
|
|||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
await _listener?.stop();
|
await _listener?.stop();
|
||||||
_rowsVisibility?.dispose();
|
_listener = null;
|
||||||
_rowsVisibility = null;
|
|
||||||
|
|
||||||
_rowsNotifier?.dispose();
|
|
||||||
_rowsNotifier = null;
|
|
||||||
|
|
||||||
_reorderAllRows?.dispose();
|
|
||||||
_reorderAllRows = null;
|
|
||||||
|
|
||||||
_reorderSingleRow?.dispose();
|
|
||||||
_reorderSingleRow = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class CalendarEventEditorBloc
|
|||||||
.firstWhere((fieldInfo) => fieldInfo.isPrimary)
|
.firstWhere((fieldInfo) => fieldInfo.isPrimary)
|
||||||
.id;
|
.id;
|
||||||
final cells = rowController
|
final cells = rowController
|
||||||
.loadData()
|
.loadCells()
|
||||||
.where(
|
.where(
|
||||||
(cellContext) =>
|
(cellContext) =>
|
||||||
_filterCellContext(cellContext, primaryFieldId),
|
_filterCellContext(cellContext, primaryFieldId),
|
||||||
@ -88,7 +88,7 @@ class CalendarEventEditorBloc
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
rowController.dispose();
|
await rowController.dispose();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,13 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
|
|||||||
_startListening();
|
_startListening();
|
||||||
await _getAllCalculations();
|
await _getAllCalculations();
|
||||||
|
|
||||||
add(
|
if (!isClosed) {
|
||||||
CalculationsEvent.didReceiveFieldUpdate(
|
add(
|
||||||
_fieldController.fieldInfos,
|
CalculationsEvent.didReceiveFieldUpdate(
|
||||||
),
|
_fieldController.fieldInfos,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
didReceiveFieldUpdate: (fields) async {
|
didReceiveFieldUpdate: (fields) async {
|
||||||
emit(
|
emit(
|
||||||
@ -131,6 +133,10 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
|
|||||||
Future<void> _getAllCalculations() async {
|
Future<void> _getAllCalculations() async {
|
||||||
final calculationsOrFailure = await _calculationsService.getCalculations();
|
final calculationsOrFailure = await _calculationsService.getCalculations();
|
||||||
|
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final RepeatedCalculationsPB? calculations =
|
final RepeatedCalculationsPB? calculations =
|
||||||
calculationsOrFailure.fold((s) => s, (e) => null);
|
calculationsOrFailure.fold((s) => s, (e) => null);
|
||||||
if (calculations != null) {
|
if (calculations != null) {
|
||||||
|
@ -36,7 +36,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
_rowController.dispose();
|
await _rowController.dispose();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
|||||||
void _init() {
|
void _init() {
|
||||||
add(
|
add(
|
||||||
RowEvent.didReceiveCells(
|
RowEvent.didReceiveCells(
|
||||||
_rowController.loadData(),
|
_rowController.loadCells(),
|
||||||
const ChangedReason.setInitialRows(),
|
const ChangedReason.setInitialRows(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
rowController.dispose();
|
await rowController.dispose();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _init() {
|
void _init() {
|
||||||
allCells.addAll(rowController.loadData());
|
allCells.addAll(rowController.loadCells());
|
||||||
int numHiddenFields = 0;
|
int numHiddenFields = 0;
|
||||||
final visibleCells = <CellContext>[];
|
final visibleCells = <CellContext>[];
|
||||||
for (final cell in allCells) {
|
for (final cell in allCells) {
|
||||||
|
@ -9,7 +9,6 @@ import 'package:appflowy/workspace/application/action_navigation/navigation_acti
|
|||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||||
@ -154,6 +153,7 @@ class _GridPageState extends State<GridPage> {
|
|||||||
finish: (result) => result.successOrFail.fold(
|
finish: (result) => result.successOrFail.fold(
|
||||||
(_) => GridShortcuts(
|
(_) => GridShortcuts(
|
||||||
child: GridPageContent(
|
child: GridPageContent(
|
||||||
|
key: ValueKey(widget.view.id),
|
||||||
view: widget.view,
|
view: widget.view,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -331,33 +331,10 @@ class _GridRowsState extends State<_GridRows> {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
GridState state,
|
GridState state,
|
||||||
) {
|
) {
|
||||||
final children = state.rowInfos.mapIndexed((index, rowInfo) {
|
// 1. GridRowBottomBar
|
||||||
return _renderRow(
|
// 2. GridCalculationsRow
|
||||||
context,
|
// 3. Footer Padding
|
||||||
rowInfo.rowId,
|
final itemCount = state.rowInfos.length + 3;
|
||||||
isDraggable: state.reorderable,
|
|
||||||
index: index,
|
|
||||||
);
|
|
||||||
}).toList()
|
|
||||||
..add(const GridRowBottomBar(key: Key('grid_footer')));
|
|
||||||
|
|
||||||
if (showFloatingCalculations) {
|
|
||||||
children.add(
|
|
||||||
const SizedBox(
|
|
||||||
key: Key('calculations_bottom_padding'),
|
|
||||||
height: 36,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
children.add(
|
|
||||||
GridCalculationsRow(
|
|
||||||
key: const Key('grid_calculations'),
|
|
||||||
viewId: widget.viewId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
children.add(const SizedBox(key: Key('footer_padding'), height: 10));
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -381,8 +358,37 @@ class _GridRowsState extends State<_GridRows> {
|
|||||||
.add(GridEvent.moveRow(fromIndex, toIndex));
|
.add(GridEvent.moveRow(fromIndex, toIndex));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemCount: children.length,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) => children[index],
|
itemBuilder: (context, index) {
|
||||||
|
if (index < state.rowInfos.length) {
|
||||||
|
return _renderRow(
|
||||||
|
context,
|
||||||
|
state.rowInfos[index].rowId,
|
||||||
|
isDraggable: state.reorderable,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == state.rowInfos.length) {
|
||||||
|
return const GridRowBottomBar(key: Key('grid_footer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == state.rowInfos.length + 1) {
|
||||||
|
if (showFloatingCalculations) {
|
||||||
|
return const SizedBox(
|
||||||
|
key: Key('calculations_bottom_padding'),
|
||||||
|
height: 36,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return GridCalculationsRow(
|
||||||
|
key: const Key('grid_calculations'),
|
||||||
|
viewId: widget.viewId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox(key: Key('footer_padding'), height: 10);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showFloatingCalculations) ...[
|
if (showFloatingCalculations) ...[
|
||||||
|
@ -6,6 +6,7 @@ class GridSize {
|
|||||||
|
|
||||||
static double get scrollBarSize => 8 * scale;
|
static double get scrollBarSize => 8 * scale;
|
||||||
static double get headerHeight => 40 * scale;
|
static double get headerHeight => 40 * scale;
|
||||||
|
static double get buttonHeight => 38 * scale;
|
||||||
static double get footerHeight => 40 * scale;
|
static double get footerHeight => 40 * scale;
|
||||||
static double get horizontalHeaderPadding =>
|
static double get horizontalHeaderPadding =>
|
||||||
PlatformExtension.isDesktop ? 40 * scale : 16 * scale;
|
PlatformExtension.isDesktop ? 40 * scale : 16 * scale;
|
||||||
|
@ -81,7 +81,7 @@ class _MobileGridRowState extends State<MobileGridRow> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
_rowController.dispose();
|
await _rowController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,8 +188,14 @@ class _RowMenuButtonState extends State<RowMenuButton> {
|
|||||||
richTooltipText: widget.isDragEnabled
|
richTooltipText: widget.isDragEnabled
|
||||||
? TextSpan(
|
? TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'),
|
TextSpan(
|
||||||
TextSpan(text: LocaleKeys.tooltip_openMenu.tr()),
|
text: '${LocaleKeys.tooltip_dragRow.tr()}\n',
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.tooltip_openMenu.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
@ -57,7 +57,7 @@ class _DatabaseViewSettingContent extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: GridSize.horizontalHeaderPadding,
|
horizontal: GridSize.horizontalHeaderPadding + 40,
|
||||||
),
|
),
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -13,42 +13,54 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {
|
|||||||
FocusNode focusNode,
|
FocusNode focusNode,
|
||||||
TextEditingController textEditingController,
|
TextEditingController textEditingController,
|
||||||
) {
|
) {
|
||||||
return Column(
|
return Container(
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
TextField(
|
border: Border.fromBorderSide(
|
||||||
controller: textEditingController,
|
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||||
readOnly: true,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onEditingComplete: () => focusNode.unfocus(),
|
|
||||||
onSubmitted: (_) => focusNode.unfocus(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 1,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: GridSize.cellContentInsets,
|
|
||||||
border: InputBorder.none,
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
enabledBorder: InputBorder.none,
|
|
||||||
errorBorder: InputBorder.none,
|
|
||||||
disabledBorder: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Row(
|
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||||
children: [
|
),
|
||||||
const Spacer(),
|
padding: const EdgeInsets.symmetric(
|
||||||
Padding(
|
horizontal: 4,
|
||||||
padding: const EdgeInsets.all(8.0),
|
vertical: 2,
|
||||||
child: SummaryCellAccessory(
|
),
|
||||||
viewId: bloc.cellController.viewId,
|
child: Column(
|
||||||
fieldId: bloc.cellController.fieldId,
|
children: [
|
||||||
rowId: bloc.cellController.rowId,
|
TextField(
|
||||||
),
|
controller: textEditingController,
|
||||||
|
readOnly: true,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onEditingComplete: () => focusNode.unfocus(),
|
||||||
|
onSubmitted: (_) => focusNode.unfocus(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
maxLines: null,
|
||||||
|
minLines: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: GridSize.cellContentInsets,
|
||||||
|
border: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
disabledBorder: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Row(
|
||||||
],
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: SummaryCellAccessory(
|
||||||
|
viewId: bloc.cellController.viewId,
|
||||||
|
fieldId: bloc.cellController.fieldId,
|
||||||
|
rowId: bloc.cellController.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,42 +13,54 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {
|
|||||||
FocusNode focusNode,
|
FocusNode focusNode,
|
||||||
TextEditingController textEditingController,
|
TextEditingController textEditingController,
|
||||||
) {
|
) {
|
||||||
return Column(
|
return Container(
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
TextField(
|
border: Border.fromBorderSide(
|
||||||
readOnly: true,
|
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||||
controller: textEditingController,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onEditingComplete: () => focusNode.unfocus(),
|
|
||||||
onSubmitted: (_) => focusNode.unfocus(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
maxLines: null,
|
|
||||||
minLines: 1,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: GridSize.cellContentInsets,
|
|
||||||
border: InputBorder.none,
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
enabledBorder: InputBorder.none,
|
|
||||||
errorBorder: InputBorder.none,
|
|
||||||
disabledBorder: InputBorder.none,
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Row(
|
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||||
children: [
|
),
|
||||||
const Spacer(),
|
padding: const EdgeInsets.symmetric(
|
||||||
Padding(
|
horizontal: 4,
|
||||||
padding: const EdgeInsets.all(8.0),
|
vertical: 2,
|
||||||
child: TranslateCellAccessory(
|
),
|
||||||
viewId: bloc.cellController.viewId,
|
child: Column(
|
||||||
fieldId: bloc.cellController.fieldId,
|
children: [
|
||||||
rowId: bloc.cellController.rowId,
|
TextField(
|
||||||
),
|
readOnly: true,
|
||||||
|
controller: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onEditingComplete: () => focusNode.unfocus(),
|
||||||
|
onSubmitted: (_) => focusNode.unfocus(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
maxLines: null,
|
||||||
|
minLines: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: GridSize.cellContentInsets,
|
||||||
|
border: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
disabledBorder: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Row(
|
||||||
],
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TranslateCellAccessory(
|
||||||
|
viewId: bloc.cellController.viewId,
|
||||||
|
fieldId: bloc.cellController.fieldId,
|
||||||
|
rowId: bloc.cellController.rowId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
@ -21,10 +18,11 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../cell/editable_cell_builder.dart';
|
import '../cell/editable_cell_builder.dart';
|
||||||
|
|
||||||
import 'accessory/cell_accessory.dart';
|
import 'accessory/cell_accessory.dart';
|
||||||
|
|
||||||
/// Display the row properties in a list. Only used in [RowDetailPage].
|
/// Display the row properties in a list. Only used in [RowDetailPage].
|
||||||
@ -165,6 +163,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|||||||
svg: FlowySvgs.drag_element_s,
|
svg: FlowySvgs.drag_element_s,
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(),
|
text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
|
||||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
|
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -47,20 +44,16 @@ class ViewTitleBarWithRow extends StatelessWidget {
|
|||||||
if (state.ancestors.isEmpty) {
|
if (state.ancestors.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
const maxWidth = WindowSizeManager.minWindowWidth - 200;
|
return SingleChildScrollView(
|
||||||
return LayoutBuilder(
|
scrollDirection: Axis.horizontal,
|
||||||
builder: (context, constraints) {
|
child: SizedBox(
|
||||||
return Visibility(
|
height: 24,
|
||||||
visible: maxWidth < constraints.maxWidth,
|
child: Row(
|
||||||
// if the width is too small, only show one view title bar without the ancestors
|
// refresh the view title bar when the ancestors changed
|
||||||
replacement: _buildRowName(),
|
key: ValueKey(state.ancestors.hashCode),
|
||||||
child: Row(
|
children: _buildViewTitles(state.ancestors),
|
||||||
// refresh the view title bar when the ancestors changed
|
),
|
||||||
key: ValueKey(state.ancestors.hashCode),
|
),
|
||||||
children: _buildViewTitles(state.ancestors),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -71,16 +64,22 @@ class ViewTitleBarWithRow extends StatelessWidget {
|
|||||||
// if the level is too deep, only show the root view, the database view and the row
|
// if the level is too deep, only show the root view, the database view and the row
|
||||||
return views.length > 2
|
return views.length > 2
|
||||||
? [
|
? [
|
||||||
_buildViewButton(views.first),
|
_buildViewButton(views[1]),
|
||||||
const FlowyText.regular('/'),
|
const FlowySvg(FlowySvgs.title_bar_divider_s),
|
||||||
const FlowyText.regular(' ... /'),
|
const FlowyText.regular(' ... '),
|
||||||
|
const FlowySvg(FlowySvgs.title_bar_divider_s),
|
||||||
_buildViewButton(views.last),
|
_buildViewButton(views.last),
|
||||||
const FlowyText.regular('/'),
|
const FlowySvg(FlowySvgs.title_bar_divider_s),
|
||||||
_buildRowName(),
|
_buildRowName(),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
...views
|
...views
|
||||||
.map((e) => [_buildViewButton(e), const FlowyText.regular('/')])
|
.map(
|
||||||
|
(e) => [
|
||||||
|
_buildViewButton(e),
|
||||||
|
const FlowySvg(FlowySvgs.title_bar_divider_s),
|
||||||
|
],
|
||||||
|
)
|
||||||
.flattened,
|
.flattened,
|
||||||
_buildRowName(),
|
_buildRowName(),
|
||||||
];
|
];
|
||||||
@ -89,9 +88,9 @@ class ViewTitleBarWithRow extends StatelessWidget {
|
|||||||
Widget _buildViewButton(ViewPB view) {
|
Widget _buildViewButton(ViewPB view) {
|
||||||
return FlowyTooltip(
|
return FlowyTooltip(
|
||||||
message: view.name,
|
message: view.name,
|
||||||
child: _ViewTitle(
|
child: ViewTitle(
|
||||||
view: view,
|
view: view,
|
||||||
behavior: _ViewTitleBehavior.uneditable,
|
behavior: ViewTitleBehavior.uneditable,
|
||||||
onUpdated: () {},
|
onUpdated: () {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -180,11 +179,14 @@ class _TitleSkin extends IEditableTextCellSkin {
|
|||||||
onTap: () {},
|
onTap: () {},
|
||||||
text: Row(
|
text: Row(
|
||||||
children: [
|
children: [
|
||||||
EmojiText(
|
if (state.icon != null) ...[
|
||||||
emoji: state.icon ?? "",
|
FlowyText.emoji(
|
||||||
fontSize: 18.0,
|
state.icon!,
|
||||||
),
|
fontSize: 14.0,
|
||||||
const HSpace(2.0),
|
figmaLineHeight: 18.0,
|
||||||
|
),
|
||||||
|
const HSpace(4.0),
|
||||||
|
],
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 180),
|
constraints: const BoxConstraints(maxWidth: 180),
|
||||||
child: FlowyText.regular(
|
child: FlowyText.regular(
|
||||||
@ -204,106 +206,6 @@ class _TitleSkin extends IEditableTextCellSkin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _ViewTitleBehavior {
|
|
||||||
editable,
|
|
||||||
uneditable,
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ViewTitle extends StatefulWidget {
|
|
||||||
const _ViewTitle({
|
|
||||||
required this.view,
|
|
||||||
this.behavior = _ViewTitleBehavior.editable,
|
|
||||||
required this.onUpdated,
|
|
||||||
}) : maxTitleWidth = 180;
|
|
||||||
|
|
||||||
final ViewPB view;
|
|
||||||
final _ViewTitleBehavior behavior;
|
|
||||||
final double maxTitleWidth;
|
|
||||||
final VoidCallback onUpdated;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ViewTitle> createState() => _ViewTitleState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ViewTitleState extends State<_ViewTitle> {
|
|
||||||
late final viewListener = ViewListener(viewId: widget.view.id);
|
|
||||||
|
|
||||||
String name = '';
|
|
||||||
String icon = '';
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
name = widget.view.name.isEmpty
|
|
||||||
? LocaleKeys.document_title_placeholder.tr()
|
|
||||||
: widget.view.name;
|
|
||||||
icon = widget.view.icon.value;
|
|
||||||
|
|
||||||
viewListener.start(
|
|
||||||
onViewUpdated: (view) {
|
|
||||||
if (name != view.name || icon != view.icon.value) {
|
|
||||||
widget.onUpdated();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
name = view.name.isEmpty
|
|
||||||
? LocaleKeys.document_title_placeholder.tr()
|
|
||||||
: view.name;
|
|
||||||
icon = view.icon.value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
viewListener.stop();
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// root view
|
|
||||||
if (widget.view.parentViewId.isEmpty) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
FlowyText.regular(name),
|
|
||||||
const HSpace(4.0),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final child = Row(
|
|
||||||
children: [
|
|
||||||
EmojiText(
|
|
||||||
emoji: icon,
|
|
||||||
fontSize: 18.0,
|
|
||||||
),
|
|
||||||
const HSpace(2.0),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: widget.maxTitleWidth,
|
|
||||||
),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
name,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Listener(
|
|
||||||
onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),
|
|
||||||
child: FlowyButton(
|
|
||||||
useIntrinsicWidth: true,
|
|
||||||
onTap: () {},
|
|
||||||
text: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RenameRowPopover extends StatefulWidget {
|
class RenameRowPopover extends StatefulWidget {
|
||||||
const RenameRowPopover({
|
const RenameRowPopover({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -39,6 +39,10 @@ class DocumentCollaboratorsBloc
|
|||||||
if (userProfile != null) {
|
if (userProfile != null) {
|
||||||
_listener.start(
|
_listener.start(
|
||||||
onDocAwarenessUpdate: (states) {
|
onDocAwarenessUpdate: (states) {
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
DocumentCollaboratorsEvent.update(
|
DocumentCollaboratorsEvent.update(
|
||||||
userProfile,
|
userProfile,
|
||||||
|
@ -2,6 +2,7 @@ library document_plugin;
|
|||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/plugins/document/document_page.dart';
|
import 'package:appflowy/plugins/document/document_page.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||||
@ -118,6 +119,8 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final fixedTitle = data?[MobileDocumentScreen.viewFixedTitle];
|
||||||
|
|
||||||
return BlocProvider<ViewInfoBloc>.value(
|
return BlocProvider<ViewInfoBloc>.value(
|
||||||
value: bloc,
|
value: bloc,
|
||||||
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||||
@ -126,6 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
view: view,
|
view: view,
|
||||||
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
|
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
|
||||||
initialSelection: initialSelection,
|
initialSelection: initialSelection,
|
||||||
|
fixedTitle: fixedTitle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||||
@ -26,6 +24,7 @@ import 'package:cross_file/cross_file.dart';
|
|||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@ -42,11 +41,13 @@ class DocumentPage extends StatefulWidget {
|
|||||||
required this.view,
|
required this.view,
|
||||||
required this.onDeleted,
|
required this.onDeleted,
|
||||||
this.initialSelection,
|
this.initialSelection,
|
||||||
|
this.fixedTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final VoidCallback onDeleted;
|
final VoidCallback onDeleted;
|
||||||
final Selection? initialSelection;
|
final Selection? initialSelection;
|
||||||
|
final String? fixedTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DocumentPage> createState() => _DocumentPageState();
|
State<DocumentPage> createState() => _DocumentPageState();
|
||||||
@ -103,6 +104,7 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
BlocProvider.value(value: documentBloc),
|
BlocProvider.value(value: documentBloc),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
|
buildWhen: _shouldRebuildDocument,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator.adaptive());
|
return const Center(child: CircularProgressIndicator.adaptive());
|
||||||
@ -195,13 +197,15 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
|
|
||||||
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
||||||
final List<XFile> imageFiles = [];
|
final List<XFile> imageFiles = [];
|
||||||
final List<XFile> otherfiles = [];
|
final List<XFile> otherFiles = [];
|
||||||
|
|
||||||
for (final file in details.files) {
|
for (final file in details.files) {
|
||||||
|
final fileName = file.name.toLowerCase();
|
||||||
if (file.mimeType?.startsWith('image/') ??
|
if (file.mimeType?.startsWith('image/') ??
|
||||||
false || imgExtensionRegex.hasMatch(file.name)) {
|
false || imgExtensionRegex.hasMatch(fileName)) {
|
||||||
imageFiles.add(file);
|
imageFiles.add(file);
|
||||||
} else {
|
} else {
|
||||||
otherfiles.add(file);
|
otherFiles.add(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +217,7 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
);
|
);
|
||||||
await editorState!.dropFiles(
|
await editorState!.dropFiles(
|
||||||
data.dropTarget!,
|
data.dropTarget!,
|
||||||
otherfiles,
|
otherFiles,
|
||||||
widget.view.id,
|
widget.view.id,
|
||||||
isLocalMode,
|
isLocalMode,
|
||||||
);
|
);
|
||||||
@ -261,6 +265,7 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
|
|
||||||
if (PlatformExtension.isMobile) {
|
if (PlatformExtension.isMobile) {
|
||||||
return DocumentImmersiveCover(
|
return DocumentImmersiveCover(
|
||||||
|
fixedTitle: widget.fixedTitle,
|
||||||
view: widget.view,
|
view: widget.view,
|
||||||
userProfilePB: userProfilePB,
|
userProfilePB: userProfilePB,
|
||||||
);
|
);
|
||||||
@ -308,4 +313,31 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldRebuildDocument(DocumentState previous, DocumentState current) {
|
||||||
|
// only rebuild the document page when the below fields are changed
|
||||||
|
// this is to prevent unnecessary rebuilds
|
||||||
|
//
|
||||||
|
// If you confirm the newly added fields should be rebuilt, please update
|
||||||
|
// this function.
|
||||||
|
if (previous.editorState != current.editorState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.forceClose != current.forceClose ||
|
||||||
|
previous.isDeleted != current.isDeleted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.userProfilePB != current.userProfilePB) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.isLoading != current.isLoading ||
|
||||||
|
previous.error != current.error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -419,7 +419,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
imageSlashMenuItem,
|
imageSlashMenuItem,
|
||||||
bulletedListSlashMenuItem,
|
bulletedListSlashMenuItem,
|
||||||
numberedListSlashMenuItem,
|
numberedListSlashMenuItem,
|
||||||
|
todoListSlashMenuItem,
|
||||||
|
dividerSlashMenuItem,
|
||||||
quoteSlashMenuItem,
|
quoteSlashMenuItem,
|
||||||
|
tableSlashMenuItem,
|
||||||
referencedDocSlashMenuItem,
|
referencedDocSlashMenuItem,
|
||||||
gridSlashMenuItem(documentBloc),
|
gridSlashMenuItem(documentBloc),
|
||||||
referencedGridSlashMenuItem,
|
referencedGridSlashMenuItem,
|
||||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
@ -31,16 +32,19 @@ class BlockAddButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: LocaleKeys.blockActions_addBelowTooltip.tr(),
|
text: LocaleKeys.blockActions_addBelowTooltip.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: Platform.isMacOS
|
text: Platform.isMacOS
|
||||||
? LocaleKeys.blockActions_addAboveMacCmd.tr()
|
? LocaleKeys.blockActions_addAboveMacCmd.tr()
|
||||||
: LocaleKeys.blockActions_addAboveCmd.tr(),
|
: LocaleKeys.blockActions_addAboveCmd.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
const TextSpan(text: ' '),
|
const TextSpan(text: ' '),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: LocaleKeys.blockActions_addAboveTooltip.tr(),
|
text: LocaleKeys.blockActions_addAboveTooltip.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -21,7 +21,6 @@ class BlockActionButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
return Align(
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
preferBelow: false,
|
|
||||||
richMessage: richMessage,
|
richMessage: richMessage,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: Platform.isWindows
|
cursor: Platform.isWindows
|
||||||
|
@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -67,11 +68,14 @@ class BlockOptionButton extends StatelessWidget {
|
|||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
buildChild: (controller) => _buildOptionButton(controller),
|
buildChild: (controller) => _buildOptionButton(context, controller),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptionButton(PopoverController controller) {
|
Widget _buildOptionButton(
|
||||||
|
BuildContext context,
|
||||||
|
PopoverController controller,
|
||||||
|
) {
|
||||||
return BlockActionButton(
|
return BlockActionButton(
|
||||||
svg: FlowySvgs.drag_element_s,
|
svg: FlowySvgs.drag_element_s,
|
||||||
richMessage: TextSpan(
|
richMessage: TextSpan(
|
||||||
@ -79,9 +83,11 @@ class BlockOptionButton extends StatelessWidget {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
// todo: customize the color to highlight the text.
|
// todo: customize the color to highlight the text.
|
||||||
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
text: LocaleKeys.document_plugins_optionAction_click.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),
|
||||||
|
style: context.tooltipTextStyle(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -100,9 +100,7 @@ class ClipboardService {
|
|||||||
|
|
||||||
for (final item in reader.items) {
|
for (final item in reader.items) {
|
||||||
final availableFormats = await item.rawReader!.getAvailableFormats();
|
final availableFormats = await item.rawReader!.getAvailableFormats();
|
||||||
Log.debug(
|
Log.info('availableFormats: $availableFormats');
|
||||||
'availableFormats: $availableFormats',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final plainText = await reader.readValue(Formats.plainText);
|
final plainText = await reader.readValue(Formats.plainText);
|
||||||
@ -115,6 +113,8 @@ class ClipboardService {
|
|||||||
image = ('jpeg', await reader.readFile(Formats.jpeg));
|
image = ('jpeg', await reader.readFile(Formats.jpeg));
|
||||||
} else if (reader.canProvide(Formats.gif)) {
|
} else if (reader.canProvide(Formats.gif)) {
|
||||||
image = ('gif', await reader.readFile(Formats.gif));
|
image = ('gif', await reader.readFile(Formats.gif));
|
||||||
|
} else if (reader.canProvide(Formats.webp)) {
|
||||||
|
image = ('webp', await reader.readFile(Formats.webp));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClipboardServiceData(
|
return ClipboardServiceData(
|
||||||
|
@ -76,6 +76,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
|||||||
image.$1,
|
image.$1,
|
||||||
image.$2!,
|
image.$2!,
|
||||||
documentId,
|
documentId,
|
||||||
|
selection: selection,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
Log.info('Pasted image');
|
Log.info('Pasted image');
|
||||||
|
@ -9,7 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/imag
|
|||||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
@ -23,6 +23,7 @@ extension PasteFromImage on EditorState {
|
|||||||
'png',
|
'png',
|
||||||
'jpeg',
|
'jpeg',
|
||||||
'gif',
|
'gif',
|
||||||
|
'webp',
|
||||||
];
|
];
|
||||||
|
|
||||||
Future<void> dropImages(
|
Future<void> dropImages(
|
||||||
@ -34,7 +35,7 @@ extension PasteFromImage on EditorState {
|
|||||||
final imageFiles = files.where(
|
final imageFiles = files.where(
|
||||||
(file) =>
|
(file) =>
|
||||||
file.mimeType?.startsWith('image/') ??
|
file.mimeType?.startsWith('image/') ??
|
||||||
false || imgExtensionRegex.hasMatch(file.name),
|
false || imgExtensionRegex.hasMatch(file.name.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final file in imageFiles) {
|
for (final file in imageFiles) {
|
||||||
@ -64,18 +65,26 @@ extension PasteFromImage on EditorState {
|
|||||||
Future<bool> pasteImage(
|
Future<bool> pasteImage(
|
||||||
String format,
|
String format,
|
||||||
Uint8List imageBytes,
|
Uint8List imageBytes,
|
||||||
String documentId,
|
String documentId, {
|
||||||
) async {
|
Selection? selection,
|
||||||
if (!supportedImageFormats.contains(format)) {
|
}) async {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final context = document.root.context;
|
final context = document.root.context;
|
||||||
|
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!supportedImageFormats.contains(format)) {
|
||||||
|
Log.info('unsupported format: $format');
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
showToastNotification(
|
||||||
|
context,
|
||||||
|
message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
|
||||||
|
|
||||||
final path = await getIt<ApplicationDataStorage>().getPath();
|
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||||
@ -105,9 +114,9 @@ extension PasteFromImage on EditorState {
|
|||||||
final errorMessage = result.$2;
|
final errorMessage = result.$2;
|
||||||
|
|
||||||
if (errorMessage != null && context.mounted) {
|
if (errorMessage != null && context.mounted) {
|
||||||
showSnackBarMessage(
|
showToastNotification(
|
||||||
context,
|
context,
|
||||||
errorMessage,
|
message: errorMessage,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -116,7 +125,7 @@ extension PasteFromImage on EditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
await insertImageNode(path);
|
await insertImageNode(path, selection: selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
await File(copyToPath).delete();
|
await File(copyToPath).delete();
|
||||||
@ -124,13 +133,55 @@ extension PasteFromImage on EditorState {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('cannot copy image file', e);
|
Log.error('cannot copy image file', e);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showSnackBarMessage(
|
showToastNotification(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> insertImageNode(
|
||||||
|
String src, {
|
||||||
|
Selection? selection,
|
||||||
|
}) async {
|
||||||
|
selection ??= this.selection;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final node = getNodeAtPath(selection.end.path);
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final transaction = this.transaction;
|
||||||
|
// if the current node is empty paragraph, replace it with image node
|
||||||
|
if (node.type == ParagraphBlockKeys.type &&
|
||||||
|
(node.delta?.isEmpty ?? false)) {
|
||||||
|
transaction
|
||||||
|
..insertNode(
|
||||||
|
node.path,
|
||||||
|
imageNode(
|
||||||
|
url: src,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..deleteNode(node);
|
||||||
|
} else {
|
||||||
|
transaction.insertNode(
|
||||||
|
node.path.next,
|
||||||
|
imageNode(
|
||||||
|
url: src,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: node.path.next,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return apply(transaction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,10 +34,12 @@ class DocumentImmersiveCover extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
required this.userProfilePB,
|
required this.userProfilePB,
|
||||||
|
this.fixedTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final UserProfilePB userProfilePB;
|
final UserProfilePB userProfilePB;
|
||||||
|
final String? fixedTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();
|
State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();
|
||||||
@ -143,6 +145,18 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
|||||||
fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily;
|
fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.fixedTitle != null) {
|
||||||
|
return FlowyText(
|
||||||
|
widget.fixedTitle!,
|
||||||
|
fontSize: 28.0,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
color:
|
||||||
|
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return AutoSizeTextField(
|
return AutoSizeTextField(
|
||||||
controller: textEditingController,
|
controller: textEditingController,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
|
@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/file_extension.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||||
import 'package:appflowy_backend/dispatch/error.dart';
|
import 'package:appflowy_backend/dispatch/error.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -39,14 +38,6 @@ Future<(String? path, String? errorMessage)> saveFileToCloudStorage(
|
|||||||
String localFilePath,
|
String localFilePath,
|
||||||
String documentId,
|
String documentId,
|
||||||
) async {
|
) async {
|
||||||
final size = localFilePath.fileSize;
|
|
||||||
if (size == null || size > 10 * 1024 * 1024) {
|
|
||||||
// 10MB
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
LocaleKeys.document_plugins_file_fileTooBigError.tr(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final documentService = DocumentService();
|
final documentService = DocumentService();
|
||||||
Log.debug("Uploading file from local path: $localFilePath");
|
Log.debug("Uploading file from local path: $localFilePath");
|
||||||
final result = await documentService.uploadFile(
|
final result = await documentService.uploadFile(
|
||||||
|
@ -7,7 +7,6 @@ import 'package:appflowy/plugins/document/application/prelude.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||||
import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/util/file_extension.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy_backend/dispatch/error.dart';
|
import 'package:appflowy_backend/dispatch/error.dart';
|
||||||
@ -47,14 +46,6 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
|||||||
String localImagePath,
|
String localImagePath,
|
||||||
String documentId,
|
String documentId,
|
||||||
) async {
|
) async {
|
||||||
final size = localImagePath.fileSize;
|
|
||||||
if (size == null || size > 10 * 1024 * 1024) {
|
|
||||||
// 10MB
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final documentService = DocumentService();
|
final documentService = DocumentService();
|
||||||
Log.debug("Uploading image local path: $localImagePath");
|
Log.debug("Uploading image local path: $localImagePath");
|
||||||
final result = await documentService.uploadFile(
|
final result = await documentService.uploadFile(
|
||||||
|
@ -121,38 +121,36 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
|||||||
final type = values[currentTabIndex];
|
final type = values[currentTabIndex];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case UploadImageType.local:
|
case UploadImageType.local:
|
||||||
return Column(
|
Widget child = UploadImageFileWidget(
|
||||||
children: [
|
allowMultipleImages: widget.allowMultipleImages,
|
||||||
Padding(
|
onPickFiles: widget.onSelectedLocalImages,
|
||||||
padding: const EdgeInsets.all(8.0),
|
);
|
||||||
child: Container(
|
if (PlatformExtension.isDesktop) {
|
||||||
alignment: Alignment.center,
|
child = Padding(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(8.0),
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: Container(
|
||||||
border: Border.all(
|
alignment: Alignment.center,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
decoration: BoxDecoration(
|
||||||
),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
border: Border.all(
|
||||||
constraints: constraints,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
UploadImageFileWidget(
|
|
||||||
allowMultipleImages: widget.allowMultipleImages,
|
|
||||||
onPickFiles: widget.onSelectedLocalImages,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
constraints: constraints,
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
// if (widget.limitMaximumImageSize) ...[
|
);
|
||||||
// FlowyText(
|
} else {
|
||||||
// LocaleKeys.document_imageBlock_maximumImageSize.tr(),
|
child = Padding(
|
||||||
// fontSize: 10.0,
|
padding: const EdgeInsets.symmetric(
|
||||||
// color: Theme.of(context).hintColor,
|
horizontal: 8.0,
|
||||||
// ),
|
vertical: 12.0,
|
||||||
// ],
|
),
|
||||||
],
|
child: child,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
|
||||||
case UploadImageType.url:
|
case UploadImageType.url:
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|