Merge remote-tracking branch 'upstream/main' into fix_reset_single_shortcut_5684
52
.github/workflows/tauri2_ci.yaml
vendored
@ -20,34 +20,34 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build-self-hosted:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cd src-tauri && cargo build
|
||||
|
||||
- name: test and lint
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint:tauri
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_web_app
|
||||
args: "--debug"
|
||||
# tauri-build-self-hosted:
|
||||
# if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
# runs-on: self-hosted
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: install frontend dependencies
|
||||
# working-directory: frontend/appflowy_web_app
|
||||
# run: |
|
||||
# mkdir dist
|
||||
# pnpm install
|
||||
# cd src-tauri && cargo build
|
||||
#
|
||||
# - name: test and lint
|
||||
# working-directory: frontend/appflowy_web_app
|
||||
# run: |
|
||||
# pnpm run lint:tauri
|
||||
#
|
||||
# - uses: tauri-apps/tauri-action@v0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# tauriScript: pnpm tauri
|
||||
# projectPath: frontend/appflowy_web_app
|
||||
# args: "--debug"
|
||||
|
||||
tauri-build-ubuntu:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
#if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
|
24
CHANGELOG.md
@ -1,4 +1,28 @@
|
||||
# Release Notes
|
||||
## Version 0.6.8 - 22/08/2024
|
||||
### New Features
|
||||
- Optimized date picker and mention block.
|
||||
- Added the ability to open database row on mobile.
|
||||
- Added the ability to invite members to workspace on mobile.
|
||||
- Added support for Monochrome theme on Android.
|
||||
- Added AI Bubble button on homepage on mobile.
|
||||
- Settings, trash, members and help & support have been moved into the settings pop up menu.
|
||||
|
||||
### Bug Fixes
|
||||
- Removed Wayland header from AppImage build
|
||||
- Fixed the issue where pasting web image on mobile failed.
|
||||
|
||||
## Version 0.6.7 - 13/08/2024
|
||||
### New Features
|
||||
- Redesigned the icon picker design on Desktop.
|
||||
- Redesigned the notification page on Mobile.
|
||||
|
||||
### Bug Fixes
|
||||
- Enhance the toolbar tooltip functionality on Desktop.
|
||||
- Enhance the slash menu user experience on Desktop.
|
||||
- Fixed the issue where list style overrides occurred during text pasting.
|
||||
- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.
|
||||
|
||||
## Version 0.6.6 - 30/07/2024
|
||||
### New Features
|
||||
- Upgrade your workspace to a premium plan to unlock more features and storage.
|
||||
|
BIN
doc/readme/desktop_guide_1.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
doc/readme/desktop_guide_2.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
doc/readme/getting_started_1.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
doc/readme/mobile_guide_1.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
doc/readme/mobile_guide_2.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
doc/readme/mobile_guide_3.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
doc/readme/mobile_guide_4.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
doc/readme/mobile_guide_5.png
Normal file
After Width: | Height: | Size: 77 KiB |
@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.6.7"
|
||||
APPFLOWY_VERSION = "0.6.8"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
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>
|
1
frontend/appflowy_flutter/assets/icons/icons.json
Normal file
0
frontend/appflowy_flutter/build.yaml
Normal file
12
frontend/appflowy_flutter/dart_dependency_validator.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# dart_dependency_validator.yaml
|
||||
|
||||
allow_pins: true
|
||||
|
||||
include:
|
||||
- "lib/**"
|
||||
|
||||
exclude:
|
||||
- "packages/**"
|
||||
|
||||
ignore:
|
||||
- analyzer
|
@ -1,93 +1,93 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
// import 'package:appflowy/env/cloud_env.dart';
|
||||
// import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
// import 'package:flutter_test/flutter_test.dart';
|
||||
// import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../shared/util.dart';
|
||||
// import '../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
// void main() {
|
||||
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('supabase auth', () {
|
||||
testWidgets('sign in with supabase', (tester) async {
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
});
|
||||
// group('supabase auth', () {
|
||||
// testWidgets('sign in with supabase', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// });
|
||||
|
||||
testWidgets('sign out with supabase', (tester) async {
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
// testWidgets('sign out with supabase', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
await tester.logout();
|
||||
// // Open the setting page and sign out
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.account);
|
||||
// await tester.logout();
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
// // Go to the sign in page again
|
||||
// await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// tester.expectToSeeGoogleLoginButton();
|
||||
// });
|
||||
|
||||
testWidgets('sign in as anonymous', (tester) async {
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapSignInAsGuest();
|
||||
// testWidgets('sign in as anonymous', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
// await tester.tapSignInAsGuest();
|
||||
|
||||
// should not see the sync setting page when sign in as anonymous
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
// // should not see the sync setting page when sign in as anonymous
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-out
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
// // Scroll to sign-out
|
||||
// await tester.scrollUntilVisible(
|
||||
// find.byType(SignInOutButton),
|
||||
// 100,
|
||||
// scrollable: find.findSettingsScrollable(),
|
||||
// );
|
||||
// await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
// tester.expectToSeeGoogleLoginButton();
|
||||
// });
|
||||
|
||||
// testWidgets('enable encryption', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: CloudType.supabase);
|
||||
// // testWidgets('enable encryption', (tester) async {
|
||||
// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
|
||||
// // await tester.tapGoogleLoginInButton();
|
||||
|
||||
// // // Open the setting page and sign out
|
||||
// // await tester.openSettings();
|
||||
// // await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// // // the switch should be off by default
|
||||
// // tester.assertEnableEncryptSwitchValue(false);
|
||||
// // await tester.toggleEnableEncrypt();
|
||||
|
||||
// // // the switch should be on after toggling
|
||||
// // tester.assertEnableEncryptSwitchValue(true);
|
||||
|
||||
// // // the switch can not be toggled back to off
|
||||
// // await tester.toggleEnableEncrypt();
|
||||
// // tester.assertEnableEncryptSwitchValue(true);
|
||||
// // });
|
||||
|
||||
// testWidgets('enable sync', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
|
||||
// // Open the setting page and sign out
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// // the switch should be off by default
|
||||
// tester.assertEnableEncryptSwitchValue(false);
|
||||
// await tester.toggleEnableEncrypt();
|
||||
// // the switch should be on by default
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
|
||||
// // the switch should be off
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||
|
||||
// // the switch should be on after toggling
|
||||
// tester.assertEnableEncryptSwitchValue(true);
|
||||
|
||||
// // the switch can not be toggled back to off
|
||||
// await tester.toggleEnableEncrypt();
|
||||
// tester.assertEnableEncryptSwitchValue(true);
|
||||
// await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
// tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
// });
|
||||
|
||||
testWidgets('enable sync', (tester) async {
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// the switch should be on by default
|
||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
|
||||
// the switch should be off
|
||||
tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||
|
||||
// the switch should be on after toggling
|
||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
// });
|
||||
// }
|
||||
|
@ -47,13 +47,12 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
await tester.enterUserName(name);
|
||||
await tester.tapEscButton();
|
||||
|
||||
// wait 2 seconds for the sync to finish
|
||||
await tester.pumpAndSettle(const Duration(seconds: 6));
|
||||
await tester.logout();
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
testWidgets('get user icon and name from server', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
@ -72,6 +71,4 @@ void main() {
|
||||
|
||||
expect(profileSetting.name, name);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -461,22 +461,22 @@ void main() {
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
await tester.createNewChecklistTask(name: "task 1", enter: true);
|
||||
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
await tester.renameChecklistTask(index: 0, name: "task 11");
|
||||
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
name: "task 11",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@ -25,6 +25,7 @@ void main() {
|
||||
const lines = 3;
|
||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
|
||||
ClipboardService.mockSetData(ClipboardServiceData(plainText: text));
|
||||
|
||||
await insertCodeBlockInDocument(tester);
|
||||
|
||||
@ -51,7 +52,9 @@ Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
|
||||
// open the actions menu and insert the codeBlock
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
LocaleKeys.document_slashMenu_name_code.tr(),
|
||||
offset: 150,
|
||||
);
|
||||
// wait for the codeBlock to be inserted
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
@ -165,6 +165,44 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
@ -7,7 +6,6 @@ import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.d
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@ -22,7 +20,7 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
|
||||
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
@ -50,7 +48,7 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Board);
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Board);
|
||||
|
||||
// validate the referenced board is inserted
|
||||
expect(
|
||||
@ -66,7 +64,7 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
|
||||
await insertLinkedDatabase(tester, ViewLayoutPB.Calendar);
|
||||
|
||||
// validate the referenced grid is inserted
|
||||
expect(
|
||||
@ -129,7 +127,7 @@ void main() {
|
||||
}
|
||||
|
||||
/// Insert a referenced database of [layout] into the document
|
||||
Future<void> insertReferenceDatabase(
|
||||
Future<void> insertLinkedDatabase(
|
||||
WidgetTester tester,
|
||||
ViewLayoutPB layout,
|
||||
) async {
|
||||
@ -150,7 +148,7 @@ Future<void> insertReferenceDatabase(
|
||||
// insert a referenced view
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
layout.referencedMenuName,
|
||||
layout.slashMenuLinkedName,
|
||||
);
|
||||
|
||||
final linkToPageMenu = find.byType(InlineActionsHandler);
|
||||
@ -176,16 +174,9 @@ Future<void> createInlineDatabase(
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// insert a referenced view
|
||||
await tester.editor.showSlashMenu();
|
||||
final name = switch (layout) {
|
||||
ViewLayoutPB.Grid => LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
|
||||
ViewLayoutPB.Board =>
|
||||
LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
|
||||
ViewLayoutPB.Calendar =>
|
||||
LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(),
|
||||
_ => '',
|
||||
};
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
name,
|
||||
layout.slashMenuName,
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -10,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -33,7 +32,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('File');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_file.tr(),
|
||||
);
|
||||
expect(find.byType(FileBlockComponent), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(FileBlockComponent));
|
||||
@ -111,7 +112,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('File');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_file.tr(),
|
||||
);
|
||||
expect(find.byType(FileBlockComponent), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(FileBlockComponent));
|
||||
|
@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -17,6 +14,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
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_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -43,7 +42,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
@ -91,7 +92,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
@ -144,7 +147,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
@ -175,7 +180,9 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
|
@ -33,7 +33,7 @@ void main() {
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
final inlineMathEquationButton = find.byTooltip(
|
||||
final inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
@ -78,7 +78,7 @@ void main() {
|
||||
);
|
||||
|
||||
// tap the inline math equation button
|
||||
var inlineMathEquationButton = find.byTooltip(
|
||||
var inlineMathEquationButton = find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
);
|
||||
await tester.tapButton(inlineMathEquationButton);
|
||||
@ -93,11 +93,11 @@ void main() {
|
||||
);
|
||||
|
||||
// expect to the see the inline math equation button is highlighted
|
||||
inlineMathEquationButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SVGIconItemWidget &&
|
||||
widget.tooltip ==
|
||||
inlineMathEquationButton = find.descendant(
|
||||
of: find.findFlowyTooltip(
|
||||
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
),
|
||||
matching: find.byType(SVGIconItemWidget),
|
||||
);
|
||||
expect(
|
||||
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,
|
||||
|
@ -1,9 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -20,6 +16,9 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -49,7 +48,10 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
|
||||
@ -144,7 +146,10 @@ void main() {
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
|
||||
|
@ -171,7 +171,8 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
||||
// open the actions menu and insert the outline block
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_selectionMenu_outline.tr(),
|
||||
LocaleKeys.document_slashMenu_name_outline.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/mock/mock_openai_repository.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
|
||||
|
||||
group('integration tests for open-ai smart menu', () {
|
||||
setUpAll(() async => service.setUpAll());
|
||||
setUp(() async => service.setUp());
|
||||
|
||||
testWidgets('testing selection on open-ai smart menu replace',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [1], offset: 4),
|
||||
end: Position(path: [1], offset: 10),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
|
||||
|
||||
await tester.tap(find.byTooltip('AI Assistants'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
editorState.service.selectionService.currentSelection.value,
|
||||
Selection(
|
||||
start: Position(path: [1], offset: 4),
|
||||
end: Position(path: [1], offset: 84),
|
||||
),
|
||||
);
|
||||
});
|
||||
testWidgets('testing selection on open-ai smart menu insert',
|
||||
(tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
final editorState = appFlowyEditor.editorState;
|
||||
|
||||
editorState.service.selectionService.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [1]),
|
||||
end: Position(path: [1], offset: 5),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
|
||||
|
||||
await tester.tap(find.byTooltip('AI Assistants'));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.tap(find.text('Summarize'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester
|
||||
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
editorState.service.selectionService.currentSelection.value,
|
||||
Selection(
|
||||
start: Position(path: [2]),
|
||||
end: Position(path: [3]),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await mockOpenAIRepository();
|
||||
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder editor = find.byType(AppFlowyEditor);
|
||||
await tester.tap(editor);
|
||||
await tester.pumpAndSettle();
|
||||
return tester.state(editor).widget as AppFlowyEditor;
|
||||
}
|
||||
|
||||
Future<void> mockOpenAIRepository() async {
|
||||
await getIt.unregister<AIRepository>();
|
||||
getIt.registerFactoryAsync<AIRepository>(
|
||||
() => Future.value(
|
||||
MockOpenAIRepository(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
@ -2,7 +2,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -52,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
assert(isSwitched == value);
|
||||
}
|
||||
|
||||
void assertEnableEncryptSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
of: find.byType(EnableEncrypt),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
),
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
void assertSupabaseEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
of: find.byType(SupabaseEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
),
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
||||
assertToggleValue(
|
||||
find.descendant(
|
||||
@ -82,15 +61,6 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleEnableEncrypt() async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(EnableEncrypt),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
);
|
||||
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> toggleEnableSync(Type syncButton) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(syncButton),
|
||||
|
@ -1,21 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/cloud_env_test.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -56,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
@ -76,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
case AuthenticatorType.local:
|
||||
await useLocalServer();
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
await useTestSupabaseCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => SupabaseMockAuthService(),
|
||||
);
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
await useTestSelfHostedAppFlowyCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
@ -231,14 +220,17 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
||||
(widget) => widget is FlowyText && widget.text == text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useTestSupabaseCloud() async {
|
||||
await useSupabaseCloud(
|
||||
url: TestEnv.supabaseUrl,
|
||||
anonKey: TestEnv.supabaseAnonKey,
|
||||
Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) {
|
||||
return byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyTooltip &&
|
||||
widget.richMessage != null &&
|
||||
widget.richMessage!.toPlainText().contains(richMessage),
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useTestSelfHostedAppFlowyCloud() async {
|
||||
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
|
||||
|
@ -662,4 +662,34 @@ extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
}
|
||||
|
||||
String get slashMenuName {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.document_slashMenu_name_grid.tr();
|
||||
case ViewLayoutPB.Board:
|
||||
return LocaleKeys.document_slashMenu_name_kanban.tr();
|
||||
case ViewLayoutPB.Document:
|
||||
return LocaleKeys.document_slashMenu_name_doc.tr();
|
||||
case ViewLayoutPB.Calendar:
|
||||
return LocaleKeys.document_slashMenu_name_calendar.tr();
|
||||
default:
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
}
|
||||
|
||||
String get slashMenuLinkedName {
|
||||
switch (this) {
|
||||
case ViewLayoutPB.Grid:
|
||||
return LocaleKeys.document_slashMenu_name_linkedGrid.tr();
|
||||
case ViewLayoutPB.Board:
|
||||
return LocaleKeys.document_slashMenu_name_linkedKanban.tr();
|
||||
case ViewLayoutPB.Document:
|
||||
return LocaleKeys.document_slashMenu_name_linkedDoc.tr();
|
||||
case ViewLayoutPB.Calendar:
|
||||
return LocaleKeys.document_slashMenu_name_linkedCalendar.tr();
|
||||
default:
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
@ -15,8 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/doc
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -85,7 +84,7 @@ class EditorOperations {
|
||||
final Finder button = !isInPicker
|
||||
? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())
|
||||
: find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
of: find.byType(FlowyIconEmojiPicker),
|
||||
matching: find.text(LocaleKeys.button_remove.tr()),
|
||||
);
|
||||
await tester.tapButton(button);
|
||||
@ -171,7 +170,10 @@ class EditorOperations {
|
||||
/// Tap the slash menu item with [name]
|
||||
///
|
||||
/// Must call [showSlashMenu] first.
|
||||
Future<void> tapSlashMenuItemWithName(String name) async {
|
||||
Future<void> tapSlashMenuItemWithName(
|
||||
String name, {
|
||||
double offset = 200,
|
||||
}) async {
|
||||
final slashMenu = find
|
||||
.ancestor(
|
||||
of: find.byType(SelectionMenuItemWidget),
|
||||
@ -181,8 +183,13 @@ class EditorOperations {
|
||||
)
|
||||
.first;
|
||||
final slashMenuItem = find.text(name, findRichText: true);
|
||||
await tester.scrollUntilVisible(slashMenuItem, 200, scrollable: slashMenu);
|
||||
// await tester.ensureVisible(slashMenuItem);
|
||||
await tester.scrollUntilVisible(
|
||||
slashMenuItem,
|
||||
offset,
|
||||
scrollable: slashMenu,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
assert(slashMenuItem.hasFound);
|
||||
await tester.tapButton(slashMenuItem);
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ extension AppFlowySettings on WidgetTester {
|
||||
of: find.byType(UserProfileSetting),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
);
|
||||
await tap(editUsernameFinder);
|
||||
await tap(editUsernameFinder, warnIfMissed: false);
|
||||
await pumpAndSettle();
|
||||
|
||||
final userNameFinder = find.descendant(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
@ -58,7 +58,7 @@ extension AppFlowyWorkspace on WidgetTester {
|
||||
);
|
||||
expect(iconButton, findsOneWidget);
|
||||
await tapButton(iconButton);
|
||||
final iconPicker = find.byType(FlowyIconPicker);
|
||||
final iconPicker = find.byType(FlowyIconEmojiPicker);
|
||||
expect(iconPicker, findsOneWidget);
|
||||
await tapButton(find.findTextInFlowyText(icon));
|
||||
}
|
||||
|
@ -48,8 +48,6 @@ PODS:
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- Toast
|
||||
- image_gallery_saver (2.0.2):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
@ -65,12 +63,15 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- printing (1.0.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
- Sentry/HybridSDK (8.33.0)
|
||||
- sentry_flutter (8.7.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.33.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@ -95,7 +96,6 @@ DEPENDENCIES:
|
||||
- flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
@ -103,7 +103,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- printing (from `.symlinks/plugins/printing/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
@ -116,6 +116,7 @@ SPEC REPOS:
|
||||
- DKPhotoGallery
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
@ -136,8 +137,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
image_gallery_saver:
|
||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
integration_test:
|
||||
@ -152,8 +151,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
printing:
|
||||
:path: ".symlinks/plugins/printing/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@ -176,7 +175,6 @@ SPEC CHECKSUMS:
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
@ -184,9 +182,10 @@ SPEC CHECKSUMS:
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
|
||||
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
|
@ -13,7 +13,6 @@ class AppFlowyConfiguration {
|
||||
required this.device_id,
|
||||
required this.platform,
|
||||
required this.authenticator_type,
|
||||
required this.supabase_config,
|
||||
required this.appflowy_cloud_config,
|
||||
required this.envs,
|
||||
});
|
||||
@ -28,41 +27,12 @@ class AppFlowyConfiguration {
|
||||
final String device_id;
|
||||
final String platform;
|
||||
final int authenticator_type;
|
||||
final SupabaseConfiguration supabase_config;
|
||||
final AppFlowyCloudConfiguration appflowy_cloud_config;
|
||||
final Map<String, String> envs;
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SupabaseConfiguration {
|
||||
SupabaseConfiguration({
|
||||
required this.url,
|
||||
required this.anon_key,
|
||||
});
|
||||
|
||||
factory SupabaseConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
_$SupabaseConfigurationFromJson(json);
|
||||
|
||||
/// Indicates whether the sync feature is enabled.
|
||||
final String url;
|
||||
final String anon_key;
|
||||
|
||||
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
|
||||
|
||||
static SupabaseConfiguration defaultConfig() {
|
||||
return SupabaseConfiguration(
|
||||
url: '',
|
||||
anon_key: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return url.isNotEmpty && anon_key.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AppFlowyCloudConfiguration {
|
||||
AppFlowyCloudConfiguration({
|
||||
|
67
frontend/appflowy_flutter/lib/env/cloud_env.dart
vendored
@ -21,9 +21,6 @@ Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
|
||||
case AuthenticatorType.local:
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
||||
break;
|
||||
@ -63,8 +60,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
switch (value ?? "0") {
|
||||
case "0":
|
||||
return AuthenticatorType.local;
|
||||
case "1":
|
||||
return AuthenticatorType.supabase;
|
||||
case "2":
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
case "3":
|
||||
@ -93,10 +88,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
/// Returns `false` otherwise.
|
||||
bool get isAuthEnabled {
|
||||
final env = getIt<AppFlowyCloudSharedEnv>();
|
||||
if (env.authenticatorType == AuthenticatorType.supabase) {
|
||||
return env.supabaseConfig.isValid;
|
||||
}
|
||||
|
||||
if (env.authenticatorType.isAppFlowyCloudEnabled) {
|
||||
return env.appflowyCloudConfig.isValid;
|
||||
}
|
||||
@ -104,19 +95,6 @@ bool get isAuthEnabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks if Supabase is enabled.
|
||||
///
|
||||
/// This getter evaluates if Supabase should be enabled based on the
|
||||
/// current integration mode and cloud type setting.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether Supabase is enabled. It returns `true`
|
||||
/// if the application is in release or develop mode and the current cloud type
|
||||
/// is `CloudType.supabase`. Otherwise, it returns `false`.
|
||||
bool get isSupabaseEnabled {
|
||||
return currentCloudType().isSupabaseEnabled;
|
||||
}
|
||||
|
||||
/// Determines if AppFlowy Cloud is enabled.
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
return currentCloudType().isAppFlowyCloudEnabled;
|
||||
@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled {
|
||||
|
||||
enum AuthenticatorType {
|
||||
local,
|
||||
supabase,
|
||||
appflowyCloud,
|
||||
appflowyCloudSelfHost,
|
||||
// The 'appflowyCloudDevelop' type is used for develop purposes only.
|
||||
@ -137,14 +114,10 @@ enum AuthenticatorType {
|
||||
this == AuthenticatorType.appflowyCloudDevelop ||
|
||||
this == AuthenticatorType.appflowyCloud;
|
||||
|
||||
bool get isSupabaseEnabled => this == AuthenticatorType.supabase;
|
||||
|
||||
int get value {
|
||||
switch (this) {
|
||||
case AuthenticatorType.local:
|
||||
return 0;
|
||||
case AuthenticatorType.supabase:
|
||||
return 1;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
return 2;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
@ -158,8 +131,6 @@ enum AuthenticatorType {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return AuthenticatorType.local;
|
||||
case 1:
|
||||
return AuthenticatorType.supabase;
|
||||
case 2:
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
case 3:
|
||||
@ -197,25 +168,15 @@ Future<void> useLocalServer() async {
|
||||
await _setAuthenticatorType(AuthenticatorType.local);
|
||||
}
|
||||
|
||||
Future<void> useSupabaseCloud({
|
||||
required String url,
|
||||
required String anonKey,
|
||||
}) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.supabase);
|
||||
await setSupabaseServer(url, anonKey);
|
||||
}
|
||||
|
||||
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
||||
class AppFlowyCloudSharedEnv {
|
||||
AppFlowyCloudSharedEnv({
|
||||
required AuthenticatorType authenticatorType,
|
||||
required this.appflowyCloudConfig,
|
||||
required this.supabaseConfig,
|
||||
}) : _authenticatorType = authenticatorType;
|
||||
|
||||
final AuthenticatorType _authenticatorType;
|
||||
final AppFlowyCloudConfiguration appflowyCloudConfig;
|
||||
final SupabaseConfiguration supabaseConfig;
|
||||
|
||||
AuthenticatorType get authenticatorType => _authenticatorType;
|
||||
|
||||
@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv {
|
||||
? await getAppFlowyCloudConfig(authenticatorType)
|
||||
: AppFlowyCloudConfiguration.defaultConfig();
|
||||
|
||||
final supabaseCloudConfig = authenticatorType.isSupabaseEnabled
|
||||
? await getSupabaseCloudConfig()
|
||||
: SupabaseConfiguration.defaultConfig();
|
||||
|
||||
// In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,
|
||||
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
|
||||
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
|
||||
@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv {
|
||||
return AppFlowyCloudSharedEnv(
|
||||
authenticatorType: authenticatorType,
|
||||
appflowyCloudConfig: appflowyCloudConfig,
|
||||
supabaseConfig: supabaseCloudConfig,
|
||||
);
|
||||
} else {
|
||||
// Using the cloud settings from the .env file.
|
||||
@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv {
|
||||
return AppFlowyCloudSharedEnv(
|
||||
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
|
||||
appflowyCloudConfig: appflowyCloudConfig,
|
||||
supabaseConfig: SupabaseConfiguration.defaultConfig(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv {
|
||||
@override
|
||||
String toString() {
|
||||
return 'authenticator: $_authenticatorType\n'
|
||||
'appflowy: ${appflowyCloudConfig.toJson()}\n'
|
||||
'supabase: ${supabaseConfig.toJson()})\n';
|
||||
'appflowy: ${appflowyCloudConfig.toJson()}\n';
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,22 +308,3 @@ Future<void> setSupabaseServer(
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
|
||||
final url = await _getSupabaseUrl();
|
||||
final anonKey = await _getSupabaseAnonKey();
|
||||
return SupabaseConfiguration(
|
||||
url: url,
|
||||
anon_key: anonKey,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getSupabaseUrl() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
|
||||
return result ?? '';
|
||||
}
|
||||
|
||||
Future<String> _getSupabaseAnonKey() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
|
||||
return result ?? '';
|
||||
}
|
||||
|
7
frontend/appflowy_flutter/lib/env/env.dart
vendored
@ -36,4 +36,11 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String internalBuild = _Env.internalBuild;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: false,
|
||||
varName: 'SENTRY_DSN',
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String sentryDsn = _Env.sentryDsn;
|
||||
}
|
||||
|
@ -2,27 +2,41 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
extension MobileRouter on BuildContext {
|
||||
Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async {
|
||||
Future<void> pushView(
|
||||
ViewPB view, {
|
||||
Map<String, dynamic>? arguments,
|
||||
bool addInRecent = true,
|
||||
bool showMoreButton = true,
|
||||
String? fixedTitle,
|
||||
}) async {
|
||||
// set the current view before pushing the new view
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));
|
||||
final queryParameters = view.queryParameters(arguments);
|
||||
|
||||
if (view.layout == ViewLayoutPB.Document) {
|
||||
queryParameters[MobileDocumentScreen.viewShowMoreButton] =
|
||||
showMoreButton.toString();
|
||||
if (fixedTitle != null) {
|
||||
queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
|
||||
}
|
||||
}
|
||||
|
||||
final uri = Uri(
|
||||
path: view.routeName,
|
||||
queryParameters: view.queryParameters(arguments),
|
||||
queryParameters: queryParameters,
|
||||
).toString();
|
||||
await push(uri);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -23,16 +24,20 @@ class NotificationReminderBloc
|
||||
await event.when(
|
||||
initial: (reminder, dateFormat, timeFormat) async {
|
||||
this.reminder = reminder;
|
||||
this.dateFormat = dateFormat;
|
||||
this.timeFormat = timeFormat;
|
||||
|
||||
add(const NotificationReminderEvent.reset());
|
||||
},
|
||||
reset: () async {
|
||||
final createdAt = await _getCreatedAt(
|
||||
reminder,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
);
|
||||
final view = await _getView(reminder);
|
||||
final node = await _getContent(reminder);
|
||||
|
||||
if (view == null || node == null) {
|
||||
if (view == null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
createdAt: createdAt,
|
||||
@ -41,7 +46,13 @@ class NotificationReminderBloc
|
||||
status: NotificationReminderStatus.error,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
}
|
||||
|
||||
final layout = view!.layout;
|
||||
|
||||
if (layout.isDocumentView) {
|
||||
final node = await _getContent(reminder);
|
||||
if (node != null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
createdAt: createdAt,
|
||||
@ -53,13 +64,25 @@ class NotificationReminderBloc
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (layout.isDatabaseView) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.name,
|
||||
view: view,
|
||||
reminderContent: reminder.message,
|
||||
status: NotificationReminderStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
reset: () {},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
late final ReminderPB reminder;
|
||||
late final UserDateFormatPB dateFormat;
|
||||
late final UserTimeFormatPB timeFormat;
|
||||
|
||||
Future<String> _getCreatedAt(
|
||||
ReminderPB reminder,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_listener.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@ -113,7 +111,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
);
|
||||
}
|
||||
|
||||
final _service = DocumentService();
|
||||
final ViewPB view;
|
||||
final DocumentListener _documentListener;
|
||||
final ViewListener _viewListener;
|
||||
@ -124,16 +121,6 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
|
||||
// for the version under 0.5.5
|
||||
Future<(CoverType, String?)> getCoverV1() async {
|
||||
final result = await _service.getDocument(documentId: view.id);
|
||||
final document = result.fold((s) => s.toDocument(), (f) => null);
|
||||
if (document != null) {
|
||||
final coverType = CoverType.fromString(
|
||||
document.root.attributes[DocumentHeaderBlockKeys.coverType],
|
||||
);
|
||||
final coverValue = document
|
||||
.root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
|
||||
return (coverType, coverValue);
|
||||
}
|
||||
return (CoverType.none, null);
|
||||
}
|
||||
|
||||
|
@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
||||
UserProfileBloc() : super(const _Initial()) {
|
||||
on<UserProfileEvent>((event, emit) async {
|
||||
await event.when(
|
||||
started: () async => _initalize(emit),
|
||||
started: () async => _initialize(emit),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initalize(Emitter<UserProfileState> emit) async {
|
||||
Future<void> _initialize(Emitter<UserProfileState> emit) async {
|
||||
emit(const UserProfileState.loading());
|
||||
|
||||
final workspaceOrFailure =
|
||||
|
@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType {
|
||||
Widget getWidget(VoidCallback? onTap) {
|
||||
switch (this) {
|
||||
case FlowyAppBarLeadingType.back:
|
||||
return AppBarBackButton(onTap: onTap);
|
||||
return AppBarImmersiveBackButton(onTap: onTap);
|
||||
case FlowyAppBarLeadingType.close:
|
||||
return AppBarCloseButton(onTap: onTap);
|
||||
case FlowyAppBarLeadingType.cancel:
|
||||
|
@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarImmersiveBackButton extends StatelessWidget {
|
||||
const AppBarImmersiveBackButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
right: 4.0,
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_app_bar_back_s,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarCloseButton extends StatelessWidget {
|
||||
const AppBarCloseButton({
|
||||
super.key,
|
||||
|
@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
@ -27,6 +27,8 @@ class MobileViewPage extends StatefulWidget {
|
||||
required this.viewLayout,
|
||||
this.title,
|
||||
this.arguments,
|
||||
this.fixedTitle,
|
||||
this.showMoreButton = true,
|
||||
});
|
||||
|
||||
/// view id
|
||||
@ -34,6 +36,10 @@ class MobileViewPage extends StatefulWidget {
|
||||
final ViewLayoutPB viewLayout;
|
||||
final String? title;
|
||||
final Map<String, dynamic>? arguments;
|
||||
final bool showMoreButton;
|
||||
|
||||
// only used in row page
|
||||
final String? fixedTitle;
|
||||
|
||||
@override
|
||||
State<MobileViewPage> createState() => _MobileViewPageState();
|
||||
@ -46,10 +52,18 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
// control the app bar opacity when in immersive mode
|
||||
final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.started());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appBarOpacity.dispose();
|
||||
_scrollNotificationObserver = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -78,8 +92,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
ViewBloc(view: view)..add(const ViewEvent.initial()),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>()
|
||||
..add(const ReminderEvent.started()),
|
||||
value: getIt<ReminderBloc>(),
|
||||
),
|
||||
if (view.layout.isDocumentView)
|
||||
BlocProvider(
|
||||
@ -157,6 +170,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return plugin.widgetBuilder.buildWidget(
|
||||
shrinkWrap: false,
|
||||
context: PluginContext(userProfile: state.userProfilePB),
|
||||
data: {
|
||||
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
|
||||
},
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
@ -209,6 +225,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
]);
|
||||
}
|
||||
|
||||
if (widget.showMoreButton) {
|
||||
actions.addAll([
|
||||
MobileViewPageMoreButton(
|
||||
view: view,
|
||||
@ -216,6 +233,11 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
appBarOpacity: _appBarOpacity,
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
actions.addAll([
|
||||
const HSpace(18.0),
|
||||
]);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
@ -225,19 +247,20 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null && icon.isNotEmpty)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(width: 34.0),
|
||||
child: EmojiText(
|
||||
emoji: '$icon ',
|
||||
fontSize: 22.0,
|
||||
),
|
||||
if (icon != null && icon.isNotEmpty) ...[
|
||||
FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 15.0,
|
||||
figmaLineHeight: 18.0,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
view?.name ?? widget.title ?? '',
|
||||
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
|
||||
fontSize: 15.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 18.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
|
||||
value.text,
|
||||
fontSize: 14.0,
|
||||
maxLines: 2,
|
||||
lineHeight: 1.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState
|
||||
height: 42.0,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
keyboardType: TextInputType.text,
|
||||
onSubmitted: (text) => widget.onRename(text),
|
||||
),
|
||||
@ -64,7 +65,7 @@ class _MobileBottomSheetRenameWidgetState
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
fontColor: Colors.white,
|
||||
textColor: Colors.white,
|
||||
fillColor: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
widget.onRename(controller.text);
|
||||
|
@ -116,12 +116,18 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
|
||||
await showFlowyCupertinoConfirmDialog(
|
||||
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
|
||||
leftButton: FlowyText.regular(
|
||||
leftButton: FlowyText(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
color: const Color(0xFF1456F0),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF007AFF),
|
||||
),
|
||||
rightButton: FlowyText.medium(
|
||||
rightButton: FlowyText(
|
||||
LocaleKeys.button_delete.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFFE0220),
|
||||
),
|
||||
onRightButtonPressed: (context) {
|
||||
|
@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
@ -294,6 +295,7 @@ class MobileRowDetailPageContentState
|
||||
RowCache get rowCache => widget.databaseController.rowCache;
|
||||
FieldController get fieldController =>
|
||||
widget.databaseController.fieldController;
|
||||
ValueNotifier<String> primaryFieldId = ValueNotifier('');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -326,7 +328,13 @@ class MobileRowDetailPageContentState
|
||||
fieldController: fieldController,
|
||||
rowMeta: rowController.rowMeta,
|
||||
)..add(const RowBannerEvent.initial()),
|
||||
child: BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
child: BlocConsumer<RowBannerBloc, RowBannerState>(
|
||||
listener: (context, state) {
|
||||
if (state.primaryField == null) {
|
||||
return;
|
||||
}
|
||||
primaryFieldId.value = state.primaryField!.id;
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.primaryField == null) {
|
||||
return const SizedBox.shrink();
|
||||
@ -366,6 +374,23 @@ class MobileRowDetailPageContentState
|
||||
if (rowDetailState.numHiddenFields != 0) ...[
|
||||
const ToggleHiddenFieldsVisibilityButton(),
|
||||
],
|
||||
const VSpace(8.0),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: primaryFieldId,
|
||||
builder: (context, primaryFieldId, child) {
|
||||
if (primaryFieldId.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return OpenRowPageButton(
|
||||
databaseController: widget.databaseController,
|
||||
cellContext: CellContext(
|
||||
rowId: rowController.rowId,
|
||||
fieldId: primaryFieldId,
|
||||
),
|
||||
documentId: rowController.rowMeta.documentId,
|
||||
);
|
||||
},
|
||||
),
|
||||
MobileRowDetailCreateFieldButton(
|
||||
viewId: viewId,
|
||||
fieldController: fieldController,
|
||||
|
@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: GridSize.headerHeight,
|
||||
maxHeight: GridSize.headerHeight,
|
||||
),
|
||||
child: TextButton.icon(
|
||||
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(vertical: 14, horizontal: 6),
|
||||
EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
),
|
||||
),
|
||||
label: FlowyText.medium(
|
||||
|
@ -87,6 +87,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 16.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -133,7 +133,7 @@ enum DatabaseViewSettings {
|
||||
filter => FlowySvgs.filter_s,
|
||||
sort => FlowySvgs.sort_ascending_s,
|
||||
board => FlowySvgs.board_s,
|
||||
calendar => FlowySvgs.date_s,
|
||||
calendar => FlowySvgs.calendar_s,
|
||||
duplicate => FlowySvgs.copy_s,
|
||||
delete => FlowySvgs.delete_s,
|
||||
};
|
||||
@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
databaseLayoutFromViewLayout(view.layout).layoutName,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -7,15 +7,21 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
super.key,
|
||||
required this.id,
|
||||
this.title,
|
||||
this.showMoreButton = true,
|
||||
this.fixedTitle,
|
||||
});
|
||||
|
||||
/// view id
|
||||
final String id;
|
||||
final String? title;
|
||||
final bool showMoreButton;
|
||||
final String? fixedTitle;
|
||||
|
||||
static const routeName = '/docs';
|
||||
static const viewId = 'id';
|
||||
static const viewTitle = 'title';
|
||||
static const viewShowMoreButton = 'show_more_button';
|
||||
static const viewFixedTitle = 'fixed_title';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
id: id,
|
||||
title: title,
|
||||
viewLayout: ViewLayoutPB.Document,
|
||||
showMoreButton: showMoreButton,
|
||||
fixedTitle: fixedTitle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -96,8 +96,7 @@ class _FavoriteViews extends StatelessWidget {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return Scrollbar(
|
||||
child: ListView.separated(
|
||||
return ListView.separated(
|
||||
key: const PageStorageKey('favorite_views_page_storage_key'),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
@ -125,7 +124,6 @@ class _FavoriteViews extends StatelessWidget {
|
||||
},
|
||||
separatorBuilder: (context, index) => const HSpace(8),
|
||||
itemCount: favoriteViews.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,7 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
||||
final workspaceId =
|
||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
||||
'';
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: HomeSpaceViewSizes.mVerticalPadding,
|
||||
@ -39,7 +38,6 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
||||
showFavorite: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,16 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// Contains Public And Private Sections
|
||||
class MobileFolders extends StatelessWidget {
|
||||
@ -35,6 +31,9 @@ class MobileFolders extends StatelessWidget {
|
||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
||||
'';
|
||||
return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
listener: (context, state) {
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.initial(
|
||||
@ -70,13 +69,7 @@ class _MobileFolderState extends State<_MobileFolder> {
|
||||
child: Column(
|
||||
children: [
|
||||
..._buildSpaceOrSection(context, state),
|
||||
const VSpace(4.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
),
|
||||
child: _TrashButton(),
|
||||
),
|
||||
const VSpace(80.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -119,28 +112,3 @@ class _MobileFolderState extends State<_MobileFolder> {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _TrashButton extends StatelessWidget {
|
||||
const _TrashButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 52,
|
||||
child: FlowyButton(
|
||||
expand: true,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_delete_s,
|
||||
),
|
||||
leftIconSize: const Size.square(18),
|
||||
iconPadding: 10.0,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.trash_text.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
onTap: () => context.push(MobileHomeTrashPage.routeName),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
@ -14,14 +16,19 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({super.key});
|
||||
@ -59,6 +66,14 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
Sentry.configureScope(
|
||||
(scope) => scope.setUser(
|
||||
SentryUser(
|
||||
id: userProfile.id.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
@ -94,11 +109,14 @@ class MobileHomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MobileHomePageState extends State<MobileHomePage> {
|
||||
Loading? loadingIndicator;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
getIt<MenuSharedState>().addLatestViewListener(_onLatestViewChange);
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.started());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -124,13 +142,42 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
child: _HomePage(userProfile: widget.userProfile),
|
||||
);
|
||||
}
|
||||
|
||||
void _onLatestViewChange() async {
|
||||
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||
}
|
||||
}
|
||||
|
||||
class _HomePage extends StatefulWidget {
|
||||
const _HomePage({required this.userProfile});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<_HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<_HomePage> {
|
||||
Loading? loadingIndicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
listener: (context, state) {
|
||||
getIt<CachedRecentService>().reset();
|
||||
mCurrentWorkspace.value = state.currentWorkspace;
|
||||
|
||||
_showResultDialog(context, state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.currentWorkspace == null) {
|
||||
@ -140,6 +187,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
final workspaceId = state.currentWorkspace!.workspaceId;
|
||||
|
||||
return Column(
|
||||
key: ValueKey('mobile_home_page_$workspaceId'),
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
@ -157,8 +205,8 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SpaceOrderBloc()
|
||||
..add(const SpaceOrderEvent.initial()),
|
||||
create: (_) =>
|
||||
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
@ -192,15 +240,63 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onLatestViewChange() async {
|
||||
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
||||
if (id == null) {
|
||||
void _showResultDialog(BuildContext context, UserWorkspaceState state) {
|
||||
final actionResult = state.actionResult;
|
||||
if (actionResult == null) {
|
||||
return;
|
||||
}
|
||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||
|
||||
Log.info('workspace action result: $actionResult');
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,9 @@ 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/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
@ -18,6 +17,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'setting/settings_popup_menu.dart';
|
||||
|
||||
class MobileHomePageHeader extends StatelessWidget {
|
||||
const MobileHomePageHeader({
|
||||
super.key,
|
||||
@ -45,15 +46,16 @@ class MobileHomePageHeader extends StatelessWidget {
|
||||
? _MobileWorkspace(userProfile: userProfile)
|
||||
: _MobileUser(userProfile: userProfile),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => context.push(
|
||||
MobileHomeSettingPage.routeName,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: FlowySvg(FlowySvgs.m_notification_settings_s),
|
||||
),
|
||||
),
|
||||
const HomePageSettingsPopupMenu(),
|
||||
// GestureDetector(
|
||||
// onTap: () => context.push(
|
||||
// MobileHomeSettingPage.routeName,
|
||||
// ),
|
||||
// child: const Padding(
|
||||
// padding: EdgeInsets.all(8.0),
|
||||
// child: FlowySvg(FlowySvgs.m_notification_settings_s),
|
||||
// ),
|
||||
// ),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
),
|
||||
@ -133,6 +135,7 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
fontSize: 16.0,
|
||||
enableEdit: false,
|
||||
alignment: Alignment.centerLeft,
|
||||
figmaLineHeight: 16.0,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
currentWorkspace.workspaceId,
|
||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
@ -79,8 +80,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
PersonalInfoSettingGroup(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
// TODO: Enable and implement along with Push Notifications
|
||||
// const NotificationsSettingGroup(),
|
||||
const WorkspaceSettingGroup(),
|
||||
const AppearanceSettingGroup(),
|
||||
const LanguageSettingGroup(),
|
||||
if (Env.enableCustomCloud) const CloudSettingGroup(),
|
||||
|
@ -68,7 +68,6 @@ class _RecentViews extends StatelessWidget {
|
||||
? const Color(0xFFE9E9EC)
|
||||
: const Color(0x1AFFFFFF);
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: Scrollbar(
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||
padding: EdgeInsets.only(
|
||||
@ -98,7 +97,6 @@ class _RecentViews extends StatelessWidget {
|
||||
separatorBuilder: (context, index) => const HSpace(8),
|
||||
itemCount: recentViews.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,143 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
enum _MobileSettingsPopupMenuItem {
|
||||
settings,
|
||||
members,
|
||||
trash,
|
||||
help,
|
||||
}
|
||||
|
||||
class HomePageSettingsPopupMenu extends StatelessWidget {
|
||||
const HomePageSettingsPopupMenu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<_MobileSettingsPopupMenuItem>(
|
||||
offset: const Offset(0, 36),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
// todo: replace it with shadows
|
||||
shadowColor: const Color(0x68000000),
|
||||
elevation: 10,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_settings_more_s,
|
||||
),
|
||||
),
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<_MobileSettingsPopupMenuItem>>[
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.settings,
|
||||
svg: FlowySvgs.m_notification_settings_s,
|
||||
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
|
||||
),
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.members,
|
||||
svg: FlowySvgs.m_settings_member_s,
|
||||
text: LocaleKeys.settings_popupMenuItem_members.tr(),
|
||||
),
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.trash,
|
||||
svg: FlowySvgs.trash_s,
|
||||
text: LocaleKeys.settings_popupMenuItem_trash.tr(),
|
||||
),
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.help,
|
||||
svg: FlowySvgs.message_support_s,
|
||||
text: LocaleKeys.settings_popupMenuItem_helpAndSupport.tr(),
|
||||
),
|
||||
],
|
||||
onSelected: (_MobileSettingsPopupMenuItem value) {
|
||||
switch (value) {
|
||||
case _MobileSettingsPopupMenuItem.members:
|
||||
_openMembersPage(context);
|
||||
break;
|
||||
case _MobileSettingsPopupMenuItem.trash:
|
||||
_openTrashPage(context);
|
||||
break;
|
||||
case _MobileSettingsPopupMenuItem.settings:
|
||||
_openSettingsPage(context);
|
||||
break;
|
||||
case _MobileSettingsPopupMenuItem.help:
|
||||
_openHelpPage(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<T> _buildItem<T>({
|
||||
required T value,
|
||||
required FlowySvgData svg,
|
||||
required String text,
|
||||
}) {
|
||||
return PopupMenuItem<T>(
|
||||
value: value,
|
||||
padding: EdgeInsets.zero,
|
||||
child: _PopupButton(
|
||||
svg: svg,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openMembersPage(BuildContext context) {
|
||||
context.push(InviteMembersScreen.routeName);
|
||||
}
|
||||
|
||||
void _openTrashPage(BuildContext context) {
|
||||
context.push(MobileHomeTrashPage.routeName);
|
||||
}
|
||||
|
||||
void _openHelpPage(BuildContext context) {
|
||||
afLaunchUrlString('https://discord.com/invite/9Q2xaN37tV');
|
||||
}
|
||||
|
||||
void _openSettingsPage(BuildContext context) {
|
||||
context.push(MobileHomeSettingPage.routeName);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopupButton extends StatelessWidget {
|
||||
const _PopupButton({
|
||||
required this.svg,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
final FlowySvgData svg;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(svg, size: const Size.square(20)),
|
||||
const HSpace(12),
|
||||
FlowyText.regular(
|
||||
text,
|
||||
fontSize: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -35,6 +35,8 @@ class MobileSpaceHeader extends StatelessWidget {
|
||||
SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
svgSize: 14,
|
||||
textDimension: 18.0,
|
||||
cornerRadius: 6.0,
|
||||
),
|
||||
const HSpace(8),
|
||||
|
@ -74,6 +74,8 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
|
||||
leftIcon: SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
svgSize: 14,
|
||||
textDimension: 18.0,
|
||||
cornerRadius: 6.0,
|
||||
),
|
||||
leftIconSize: const Size.square(24),
|
||||
|
@ -0,0 +1,81 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FloatingAIEntry extends StatelessWidget {
|
||||
const FloatingAIEntry({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedGestureDetector(
|
||||
scaleFactor: 0.99,
|
||||
onTapUp: () => mobileCreateNewAIChatNotifier.value =
|
||||
mobileCreateNewAIChatNotifier.value + 1,
|
||||
child: DecoratedBox(
|
||||
decoration: _buildShadowDecoration(context),
|
||||
child: Container(
|
||||
decoration: _buildWrapperDecoration(context),
|
||||
height: 48,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 18),
|
||||
child: _buildHintText(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _buildShadowDecoration(BuildContext context) {
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20,
|
||||
spreadRadius: 1,
|
||||
offset: const Offset(0, 4),
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _buildWrapperDecoration(BuildContext context) {
|
||||
final outlineColor = Theme.of(context).colorScheme.outline;
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? outlineColor.withOpacity(0.7)
|
||||
: outlineColor.withOpacity(0.3);
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: borderColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHintText(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.toolbar_item_ai_s,
|
||||
size: const Size.square(16.0),
|
||||
color: Theme.of(context).hintColor,
|
||||
opacity: 0.7,
|
||||
),
|
||||
const HSpace(8),
|
||||
FlowyText(
|
||||
LocaleKeys.chat_inputMessageHint.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -6,9 +6,12 @@ import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dar
|
||||
import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -17,6 +20,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'ai_bubble_button.dart';
|
||||
|
||||
final ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);
|
||||
|
||||
class MobileSpaceTab extends StatefulWidget {
|
||||
const MobileSpaceTab({
|
||||
super.key,
|
||||
@ -37,14 +44,19 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
mobileCreateNewPageNotifier.addListener(_createNewPage);
|
||||
mobileCreateNewPageNotifier.addListener(_createNewDocument);
|
||||
mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);
|
||||
mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController?.removeListener(_onTabChange);
|
||||
tabController?.dispose();
|
||||
mobileCreateNewPageNotifier.removeListener(_createNewPage);
|
||||
|
||||
mobileCreateNewPageNotifier.removeListener(_createNewDocument);
|
||||
mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);
|
||||
mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
@ -140,7 +152,20 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
case MobileSpaceTabType.recent:
|
||||
return const MobileRecentSpace();
|
||||
case MobileSpaceTabType.spaces:
|
||||
return MobileHomeSpace(userProfile: widget.userProfile);
|
||||
return Stack(
|
||||
children: [
|
||||
MobileHomeSpace(userProfile: widget.userProfile),
|
||||
// only show ai chat button for cloud user
|
||||
if (widget.userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud)
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: const FloatingAIEntry(),
|
||||
),
|
||||
],
|
||||
);
|
||||
case MobileSpaceTabType.favorites:
|
||||
return MobileFavoriteSpace(userProfile: widget.userProfile);
|
||||
default:
|
||||
@ -150,15 +175,24 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
}
|
||||
|
||||
// quick create new page when clicking the add button in navigation bar
|
||||
void _createNewPage() {
|
||||
void _createNewDocument() {
|
||||
_createNewPage(ViewLayoutPB.Document);
|
||||
}
|
||||
|
||||
void _createNewAIChat() {
|
||||
_createNewPage(ViewLayoutPB.Chat);
|
||||
}
|
||||
|
||||
void _createNewPage(ViewLayoutPB layout) {
|
||||
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
layout: layout,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
} else if (layout == ViewLayoutPB.Document) {
|
||||
// only support create document in section
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.createRootViewInSection(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
@ -171,4 +205,16 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _leaveWorkspace() {
|
||||
final workspaceId =
|
||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;
|
||||
if (workspaceId == null) {
|
||||
Log.error('Workspace ID is null');
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<UserWorkspaceBloc>()
|
||||
.add(UserWorkspaceEvent.leaveWorkspace(workspaceId));
|
||||
}
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ class _WorkspaceMenuItem extends StatelessWidget {
|
||||
enableEdit: false,
|
||||
iconSize: 26,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 16.0,
|
||||
workspace: workspace,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
||||
import 'package:appflowy/shared/red_dot.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
@ -162,7 +163,7 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
const Positioned(
|
||||
top: 2,
|
||||
right: 4,
|
||||
child: _RedDot(),
|
||||
child: NotificationRedDot(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -172,25 +173,6 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RedDot extends StatelessWidget {
|
||||
const _RedDot();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: const Color(0xFFFF2214),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomePageNavigationBar extends StatelessWidget {
|
||||
const _HomePageNavigationBar({
|
||||
required this.navigationShell,
|
||||
@ -230,11 +212,13 @@ class _HomePageNavigationBar extends StatelessWidget {
|
||||
/// Navigate to the current location of the branch at the provided index when
|
||||
/// tapping an item in the BottomNavigationBar.
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
if (_items[bottomBarIndex].label == _addLabel) {
|
||||
final label = _items[bottomBarIndex].label;
|
||||
if (label == _addLabel) {
|
||||
// show an add dialog
|
||||
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
|
||||
return;
|
||||
} else if (label == _notificationLabel) {
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
// method, as doing so makes sure the last navigation state of the
|
||||
|
@ -18,7 +18,7 @@ class MobileNotificationPageHeader extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(16.0),
|
||||
const HSpace(18.0),
|
||||
FlowyText(
|
||||
LocaleKeys.settings_notifications_titles_notifications.tr(),
|
||||
fontSize: 20,
|
||||
|
@ -6,6 +6,8 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/color.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -76,7 +78,7 @@ class UnreadRedDot extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationContent extends StatelessWidget {
|
||||
class NotificationContent extends StatefulWidget {
|
||||
const NotificationContent({
|
||||
super.key,
|
||||
required this.reminder,
|
||||
@ -84,10 +86,29 @@ class NotificationContent extends StatelessWidget {
|
||||
|
||||
final ReminderPB reminder;
|
||||
|
||||
@override
|
||||
State<NotificationContent> createState() => _NotificationContentState();
|
||||
}
|
||||
|
||||
class _NotificationContentState extends State<NotificationContent> {
|
||||
@override
|
||||
void didUpdateWidget(covariant NotificationContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
context.read<NotificationReminderBloc>().add(
|
||||
const NotificationReminderEvent.reset(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NotificationReminderBloc, NotificationReminderState>(
|
||||
builder: (context, state) {
|
||||
final view = state.view;
|
||||
if (view == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -105,15 +126,7 @@ class NotificationContent extends StatelessWidget {
|
||||
// content
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: IntrinsicHeight(
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentPageStyleBloc(view: state.view!),
|
||||
child: NotificationDocumentContent(
|
||||
reminder: reminder,
|
||||
nodes: state.nodes,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildContent(view, nodes: state.nodes),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -121,6 +134,33 @@ class NotificationContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ViewPB view, {List<Node>? nodes}) {
|
||||
if (view.layout.isDocumentView && nodes != null) {
|
||||
return IntrinsicHeight(
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentPageStyleBloc(view: view),
|
||||
child: NotificationDocumentContent(
|
||||
reminder: widget.reminder,
|
||||
nodes: nodes,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (view.layout.isDatabaseView) {
|
||||
final opacity = widget.reminder.type == ReminderType.past ? 0.3 : 1.0;
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: FlowyText(
|
||||
widget.reminder.message,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 22,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return FlowyText.semibold(
|
||||
LocaleKeys.settings_notifications_titles_reminder.tr(),
|
||||
|
@ -68,17 +68,16 @@ class MobileNotificationTabBar extends StatelessWidget {
|
||||
controller: tabController,
|
||||
tabs: tabs.map((e) => Tab(text: e.tr)).toList(),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: Theme.of(context).primaryColor,
|
||||
isScrollable: true,
|
||||
labelStyle: labelStyle,
|
||||
labelColor: baseStyle?.color,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
indicator: RoundUnderlineTabIndicator(
|
||||
indicator: const RoundUnderlineTabIndicator(
|
||||
width: 28.0,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Color(0xFF00C8FF),
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
|
@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
name,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
languageFromLocale(locale),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -7,7 +7,8 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/share_log_files.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -74,10 +75,14 @@ class SupportSettingGroup extends StatelessWidget {
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
onActionButtonPressed: () async {
|
||||
await getIt<FlowyCacheManager>().clearAllCache();
|
||||
// check the workspace and space health
|
||||
await WorkspaceDataManager.checkViewHealth(
|
||||
dryRun: false,
|
||||
);
|
||||
if (context.mounted) {
|
||||
showSnackBarMessage(
|
||||
showToastNotification(
|
||||
context,
|
||||
LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||
message: LocaleKeys.settings_files_clearCacheSuccess.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -42,14 +42,24 @@ class UserSessionSettingGroup extends StatelessWidget {
|
||||
MobileSignInOrLogoutButton(
|
||||
labelText: LocaleKeys.settings_menu_logout.tr(),
|
||||
onPressed: () async {
|
||||
await showFlowyMobileConfirmDialog(
|
||||
context,
|
||||
content: FlowyText(
|
||||
LocaleKeys.settings_menu_logoutPrompt.tr(),
|
||||
await showFlowyCupertinoConfirmDialog(
|
||||
title: LocaleKeys.settings_menu_logoutPrompt.tr(),
|
||||
leftButton: FlowyText(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF007AFF),
|
||||
),
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
actionButtonColor: Theme.of(context).colorScheme.error,
|
||||
onActionButtonPressed: () async {
|
||||
rightButton: FlowyText(
|
||||
LocaleKeys.button_logout.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFFE0220),
|
||||
),
|
||||
onRightButtonPressed: (context) async {
|
||||
Navigator.of(context).pop();
|
||||
await getIt<AuthService>().signOut();
|
||||
await runAppFlowy();
|
||||
},
|
||||
|
@ -0,0 +1,350 @@
|
||||
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_inviteFailedMemberLimitMobile
|
||||
.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_inviteFailedMemberLimitMobile
|
||||
.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_inviteFailedMemberLimitMobile
|
||||
.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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|