Merge remote-tracking branch 'upstream/main' into fix_reset_single_shortcut_5684

This commit is contained in:
MayurSMahajan 2024-08-24 14:30:07 +05:30
commit cbc735ae14
1840 changed files with 34409 additions and 15232 deletions

View File

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

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

File diff suppressed because one or more lines are too long

View File

View File

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

View File

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

View File

@ -47,13 +47,12 @@ void main() {
await tester.openSettingsPage(SettingsPage.account);
await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
await tester.pumpAndSettle(const Duration(seconds: 6));
await tester.logout();
await tester.pumpAndSettle(const Duration(seconds: 2));
});
});
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
@ -72,6 +71,4 @@ void main() {
expect(profileSetting.name, name);
});
});
}

View File

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

View File

@ -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();
}

View File

@ -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();

View File

@ -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();

View File

@ -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));

View File

@ -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(

View File

@ -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,

View File

@ -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);

View File

@ -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();
}

View File

@ -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;
}

View File

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

View File

@ -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);

View File

@ -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');
}
}
}

View File

@ -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);
}

View File

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

View File

@ -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));
}

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -27,6 +27,8 @@ class MobileViewPage extends StatefulWidget {
required this.viewLayout,
this.title,
this.arguments,
this.fixedTitle,
this.showMoreButton = true,
});
/// view id
@ -34,6 +36,10 @@ class MobileViewPage extends StatefulWidget {
final ViewLayoutPB viewLayout;
final String? title;
final Map<String, dynamic>? arguments;
final bool showMoreButton;
// only used in row page
final String? fixedTitle;
@override
State<MobileViewPage> createState() => _MobileViewPageState();
@ -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,
),
),
],

View File

@ -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,
),

View File

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

View File

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

View File

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

View File

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

View File

@ -87,6 +87,7 @@ class _PropertyCellState extends State<_PropertyCell> {
fieldInfo.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
figmaLineHeight: 16.0,
color: Theme.of(context).hintColor,
),
),

View File

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

View File

@ -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,
),

View File

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

View File

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

View File

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

View File

@ -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),
),
);
}
}

View File

@ -1,8 +1,10 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
@ -14,14 +16,19 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:sentry/sentry.dart';
import 'package:toastification/toastification.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@ -59,6 +66,14 @@ class MobileHomeScreen extends StatelessWidget {
return const WorkspaceFailedScreen();
}
Sentry.configureScope(
(scope) => scope.setUser(
SentryUser(
id: userProfile.id.toString(),
),
),
);
return Scaffold(
body: SafeArea(
bottom: false,
@ -94,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);
}
}
}

View File

@ -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,

View File

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

View File

@ -68,7 +68,6 @@ class _RecentViews extends StatelessWidget {
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return SlidableAutoCloseBehavior(
child: Scrollbar(
child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'),
padding: EdgeInsets.only(
@ -98,7 +97,6 @@ class _RecentViews extends StatelessWidget {
separatorBuilder: (context, index) => const HSpace(8),
itemCount: recentViews.length,
),
),
);
}
}

View File

@ -0,0 +1,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,
),
],
),
);
}
}

View File

@ -35,6 +35,8 @@ class MobileSpaceHeader extends StatelessWidget {
SpaceIcon(
dimension: 24,
space: space,
svgSize: 14,
textDimension: 18.0,
cornerRadius: 6.0,
),
const HSpace(8),

View File

@ -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),

View File

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

View File

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

View File

@ -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(

View File

@ -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

View File

@ -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,

View File

@ -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(),

View File

@ -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,
),
),

View File

@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
name,
color: theme.colorScheme.onSurface,
),

View File

@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
languageFromLocale(locale),
color: theme.colorScheme.onSurface,
),

View File

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

View File

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

View File

@ -0,0 +1,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;
},
);
}
}

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