diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart index 35ccf0ada0..0c8b96fa20 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -3,21 +3,28 @@ import 'dart:io'; import 'dart:ui'; +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:intl/intl.dart'; import 'package:path/path.dart' as p; +import '../desktop/board/board_hide_groups_test.dart'; import '../shared/dir.dart'; import '../shared/mock/mock_file_picker.dart'; import '../shared/util.dart'; @@ -37,22 +44,35 @@ void main() { // reanme the name of the anon user await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - final userNameFinder = find.descendant( - of: find.byType(SettingsUserView), - matching: find.byType(UserNameInput), - ); - await tester.enterText(userNameFinder, 'local_user'); - await tester.openSettingsPage(SettingsPage.user); + await tester.openSettingsPage(SettingsPage.account); await tester.pumpAndSettle(); + await tester.enterUserName('local_user'); + + // Scroll to sign-in + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + + await tester.tapButton(find.byType(SignInOutButton)); + // sign up with Google await tester.tapGoogleLoginInButton(); // sign out await tester.expectToSeeHomePage(); await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); + await tester.openSettingsPage(SettingsPage.account); + + // Scroll to sign-out + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + await tester.logout(); await tester.pumpAndSettle(); @@ -63,8 +83,9 @@ void main() { // New anon user name await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - final userNameInput = tester.widget(userNameFinder) as UserNameInput; + await tester.openSettingsPage(SettingsPage.account); + final userNameInput = + tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; expect(userNameInput.name, 'Me'); }); }); diff --git a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart index 6abdb968a1..5aa3a02d83 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart @@ -6,8 +6,8 @@ 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/workspace/application/settings/prelude.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/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -37,11 +37,18 @@ void main() { // Open the setting page and sign out await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - await tester.tapButton(find.byType(SettingLogoutButton)); + await tester.openSettingsPage(SettingsPage.account); - tester.expectToSeeText(LocaleKeys.button_ok.tr()); - await tester.tapButtonWithName(LocaleKeys.button_ok.tr()); + // Scroll to sign-out + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + await tester.tapButton(find.byType(SignInOutButton)); + + tester.expectToSeeText(LocaleKeys.button_confirm.tr()); + await tester.tapButtonWithName(LocaleKeys.button_confirm.tr()); // Go to the sign in page again await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -56,7 +63,16 @@ void main() { // should not see the sync setting page when sign in as anonymous await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); + await tester.openSettingsPage(SettingsPage.account); + + // Scroll to sign-in + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + await tester.tapButton(find.byType(SignInOutButton)); + tester.expectToSeeGoogleLoginButton(); }); diff --git a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart index e727e0bcec..c5f2c0d1aa 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart @@ -8,14 +8,15 @@ 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/workspace/application/settings/prelude.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/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + import '../shared/dir.dart'; import '../shared/mock/mock_file_picker.dart'; import '../shared/util.dart'; @@ -50,7 +51,7 @@ void main() { await tester.waitForSeconds(6); await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); + await tester.openSettingsPage(SettingsPage.account); await tester.logout(); }); diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart index 283e55ce4e..15c9c3c347 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart @@ -1,9 +1,7 @@ import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.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:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -25,11 +23,8 @@ void main() { // Open the setting page and sign out await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - await tester.tapButton(find.byType(SettingLogoutButton)); - - tester.expectToSeeText(LocaleKeys.button_ok.tr()); - await tester.tapButtonWithName(LocaleKeys.button_ok.tr()); + await tester.openSettingsPage(SettingsPage.account); + await tester.logout(); // Go to the sign in page again await tester.pumpAndSettle(const Duration(seconds: 1)); @@ -42,7 +37,16 @@ void main() { // should not see the sync setting page when sign in as anonymous await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); + 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)); + tester.expectToSeeGoogleLoginButton(); }); diff --git a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart index 1727669876..5791803a0e 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart @@ -2,22 +2,27 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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/workspace/application/settings/prelude.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/settings_user_view.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../desktop/board/board_hide_groups_test.dart'; import '../shared/database_test_op.dart'; import '../shared/dir.dart'; import '../shared/emoji.dart'; @@ -39,28 +44,9 @@ void main() { await tester.expectToSeeHomePageWithGetStartedPage(); await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - // final userAvatarFinder = find.descendant( - // of: find.byType(SettingsUserView), - // matching: find.byType(UserAvatar), - // ); + await tester.openSettingsPage(SettingsPage.account); - // Open icon picker dialog and select emoji - // await tester.tap(userAvatarFinder); - // await tester.pumpAndSettle(); - // await tester.tapEmoji('😁'); - // await tester.pumpAndSettle(); - // final UserAvatar userAvatar = - // tester.widget(userAvatarFinder) as UserAvatar; - // expect(userAvatar.iconUrl, '😁'); - - // enter user name - final userNameFinder = find.descendant( - of: find.byType(SettingsUserView), - matching: find.byType(UserNameInput), - ); - await tester.enterText(userNameFinder, name); - await tester.pumpAndSettle(); + await tester.enterUserName(name); await tester.tapEscButton(); // wait 2 seconds for the sync to finish @@ -78,23 +64,12 @@ void main() { await tester.pumpAndSettle(); await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); + await tester.openSettingsPage(SettingsPage.account); - // verify icon - // final userAvatarFinder = find.descendant( - // of: find.byType(SettingsUserView), - // matching: find.byType(UserAvatar), - // ); - // final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar; - // expect(userAvatar.iconUrl, '😁'); + // Verify name + final profileSetting = + tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; - // verify name - final userNameFinder = find.descendant( - of: find.byType(SettingsUserView), - matching: find.byType(UserNameInput), - ); - final UserNameInput userNameInput = - tester.widget(userNameFinder) as UserNameInput; - expect(userNameInput.name, name); + expect(profileSetting.name, name); }); } diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart index 5e0122c5ef..75e420baac 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart @@ -9,7 +9,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index e4afed9860..ddfd86acb1 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -2,6 +2,8 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; @@ -14,12 +16,10 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_worksp import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart index 6ef7c8b793..8e3724a583 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart @@ -14,7 +14,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart index c1cf5102d3..65f48a87ff 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart @@ -11,7 +11,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart index e80a3e9467..873e6244b6 100644 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart @@ -1,25 +1,33 @@ +import 'package:flutter/material.dart'; + 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/settings/widgets/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'base.dart'; -import 'expectation.dart'; +import 'util.dart'; extension AppFlowyAuthTest on WidgetTester { Future tapGoogleLoginInButton() async { await tapButton(find.byKey(const Key('signInWithGoogleButton'))); } + /// Requires being on the SettingsPage.account of the SettingsDialog Future logout() async { - await tapButton(find.byType(SettingLogoutButton)); + final scrollable = find.findSettingsScrollable(); + await scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: scrollable, + ); - expectToSeeText(LocaleKeys.button_ok.tr()); - await tapButtonWithName(LocaleKeys.button_ok.tr()); + await tapButton(find.byType(SignInOutButton)); + + expectToSeeText(LocaleKeys.button_confirm.tr()); + await tapButtonWithName(LocaleKeys.button_confirm.tr()); } Future tapSignInAsGuest() async { diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index 4199d3724d..47f5337fce 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -1,6 +1,9 @@ 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'; @@ -13,16 +16,12 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget 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'; class FlowyTestContext { - FlowyTestContext({ - required this.applicationDataDirectory, - }); + FlowyTestContext({required this.applicationDataDirectory}); final String applicationDataDirectory; } @@ -75,7 +74,7 @@ extension AppFlowyTestBase on WidgetTester { if (cloudType != null) { switch (cloudType) { case AuthenticatorType.local: - await useLocal(); + await useLocalServer(); break; case AuthenticatorType.supabase: await useTestSupabaseCloud(); @@ -187,37 +186,14 @@ extension AppFlowyTestBase on WidgetTester { ); } - Future tapButtonWithName( - String tr, { - int milliseconds = 500, - }) async { - Finder button = find.text( - tr, - findRichText: true, - skipOffstage: false, - ); + Future tapButtonWithName(String tr, {int milliseconds = 500}) async { + Finder button = find.text(tr, findRichText: true, skipOffstage: false); if (button.evaluate().isEmpty) { button = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.text == tr, ); } - await tapButton( - button, - milliseconds: milliseconds, - ); - return; - } - - Future tapButtonWithTooltip( - String tr, { - int milliseconds = 500, - }) async { - final button = find.byTooltip(tr); - await tapButton( - button, - milliseconds: milliseconds, - ); - return; + await tapButton(button, milliseconds: milliseconds); } Future doubleTapAt( @@ -232,34 +208,8 @@ extension AppFlowyTestBase on WidgetTester { await pumpAndSettle(Duration(milliseconds: milliseconds)); } - Future doubleTapButton( - Finder finder, { - int? pointer, - int buttons = kPrimaryButton, - bool warnIfMissed = true, - int milliseconds = 500, - }) async { - await tap( - finder, - pointer: pointer, - buttons: buttons, - warnIfMissed: warnIfMissed, - ); - - await pump(kDoubleTapMinTime); - - await tap( - finder, - buttons: buttons, - pointer: pointer, - warnIfMissed: warnIfMissed, - ); - await pumpAndSettle(Duration(milliseconds: milliseconds)); - } - Future wait(int milliseconds) async { await pumpAndSettle(Duration(milliseconds: milliseconds)); - return; } } @@ -271,10 +221,6 @@ extension AppFlowyFinderTestBase on CommonFinders { } } -Future useLocal() async { - await useLocalServer(); -} - Future useTestSupabaseCloud() async { await useSupabaseCloud( url: TestEnv.supabaseUrl, diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 25fd96e585..0a0858beec 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -24,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/log.dart'; @@ -72,27 +73,6 @@ extension CommonOperations on WidgetTester { await tapButton(newPageButton); } - /// Tap the create document button. - /// - /// Must call [tapAddViewButton] first. - Future tapCreateDocumentButton() async { - await tapButtonWithName(LocaleKeys.document_menuName.tr()); - } - - /// Tap the create grid button. - /// - /// Must call [tapAddViewButton] first. - Future tapCreateGridButton() async { - await tapButtonWithName(LocaleKeys.grid_menuName.tr()); - } - - /// Tap the create grid button. - /// - /// Must call [tapAddViewButton] first. - Future tapCreateCalendarButton() async { - await tapButtonWithName(LocaleKeys.calendar_menuName.tr()); - } - /// Tap the import button. /// /// Must call [tapAddViewButton] first. @@ -181,15 +161,9 @@ extension CommonOperations on WidgetTester { }) async { final pageNames = findPageName(name, layout: layout); if (useLast) { - await hoverOnWidget( - pageNames.last, - onHover: onHover, - ); + await hoverOnWidget(pageNames.last, onHover: onHover); } else { - await hoverOnWidget( - pageNames.first, - onHover: onHover, - ); + await hoverOnWidget(pageNames.first, onHover: onHover); } } @@ -497,9 +471,7 @@ extension CommonOperations on WidgetTester { await pumpAndSettle(); } - Future openNotificationHub({ - int tabIndex = 0, - }) async { + Future openNotificationHub({int tabIndex = 0}) async { final finder = find.descendant( of: find.byType(NotificationButton), matching: find.byWidgetPredicate( @@ -542,15 +514,6 @@ extension CommonOperations on WidgetTester { await tapButton(workspace, milliseconds: 2000); } - Future closeCollaborativeWorkspaceMenu() async { - if (!FeatureFlag.collaborativeWorkspace.isOn) { - throw UnsupportedError('Collaborative workspace is not enabled'); - } - - await tapAt(Offset.zero); - await pumpAndSettle(); - } - Future createCollaborativeWorkspace(String name) async { if (!FeatureFlag.collaborativeWorkspace.isOn) { throw UnsupportedError('Collaborative workspace is not enabled'); @@ -576,6 +539,20 @@ extension CommonOperations on WidgetTester { } } +extension SettingsFinder on CommonFinders { + Finder findSettingsScrollable() => find + .descendant( + of: find + .descendant( + of: find.byType(SettingsBody), + matching: find.byType(SingleChildScrollView), + ) + .first, + matching: find.byType(Scrollable), + ) + .first; +} + extension ViewLayoutPBTest on ViewLayoutPB { String get menuName { switch (this) { diff --git a/frontend/appflowy_flutter/integration_test/shared/data.dart b/frontend/appflowy_flutter/integration_test/shared/data.dart index 5532904aca..6a2ad830bb 100644 --- a/frontend/appflowy_flutter/integration_test/shared/data.dart +++ b/frontend/appflowy_flutter/integration_test/shared/data.dart @@ -1,8 +1,9 @@ import 'dart:io'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv_keys.dart'; import 'package:archive/archive_io.dart'; -import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -51,11 +52,7 @@ class TestWorkspaceService { Future setUpAll() async { final root = await workspace.root; final path = root.path; - SharedPreferences.setMockInitialValues( - { - KVKeys.pathLocation: path, - }, - ); + SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path}); } /// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control. diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 7585432848..6b5b0cc1cf 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; -import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -31,8 +29,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart'; @@ -57,6 +53,10 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; @@ -71,7 +71,6 @@ import 'package:appflowy/plugins/database/widgets/setting/setting_property_list. import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -343,16 +342,6 @@ extension AppFlowyDatabaseTest on WidgetTester { return w.isToday; } - Future toggleDateRange() async { - final findDateEditor = find.byType(EndTimeButton); - final findToggle = find.byType(Toggle); - final finder = find.descendant( - of: findDateEditor, - matching: findToggle, - ); - await tapButton(finder); - } - Future tapChangeDateTimeFormatButton() async { await tapButton(find.byType(DateTypeOptionButton)); } @@ -403,9 +392,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } /// The [SelectOptionCellEditor] must be opened first. - Future createOption({ - required String name, - }) async { + Future createOption({required String name}) async { final findEditor = find.byType(SelectOptionCellEditor); expect(findEditor, findsOneWidget); @@ -419,9 +406,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); } - Future selectOption({ - required String name, - }) async { + Future selectOption({required String name}) async { final option = find.byWidgetPredicate( (widget) => widget is SelectOptionTagCell && widget.option.name == name, ); @@ -440,11 +425,7 @@ extension AppFlowyDatabaseTest on WidgetTester { (widget.name == name || widget.option?.name == name), ); - final cell = find.descendant( - of: findRow.at(rowIndex), - matching: option, - ); - + final cell = find.descendant(of: findRow.at(rowIndex), matching: option); expect(cell, findsOneWidget); } @@ -458,11 +439,7 @@ extension AppFlowyDatabaseTest on WidgetTester { (widget) => widget is SelectOptionTag, ); - final cell = find.descendant( - of: findRow.at(rowIndex), - matching: options, - ); - + final cell = find.descendant(of: findRow.at(rowIndex), matching: options); expect(cell, matcher); } @@ -470,21 +447,16 @@ extension AppFlowyDatabaseTest on WidgetTester { final findRow = find.byType(GridRow); final findCell = finderForFieldType(FieldType.Checklist); - final cell = find.descendant( - of: findRow.at(rowIndex), - matching: findCell, - ); - + final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell); await tapButton(cell); } void assertChecklistEditorVisible({required bool visible}) { final editor = find.byType(ChecklistCellEditor); if (visible) { - expect(editor, findsOneWidget); - } else { - expect(editor, findsNothing); + return expect(editor, findsOneWidget); } + expect(editor, findsNothing); } Future createNewChecklistTask({ @@ -519,7 +491,6 @@ extension AppFlowyDatabaseTest on WidgetTester { required bool isChecked, }) { final task = find.byType(ChecklistItem).at(index); - final widget = this.widget(task); assert( widget.task.data.name == name && widget.task.isSelected == isChecked, @@ -591,27 +562,16 @@ extension AppFlowyDatabaseTest on WidgetTester { } } - Future editTitleInRowDetailPage(String title) async { - final titleField = find.byType(EditableTextCell); - await enterText(titleField, title); - await pumpAndSettle(); - } - Future hoverRowBanner() async { final banner = find.byType(RowBanner); expect(banner, findsOneWidget); - await startGesture( - getCenter(banner), - kind: PointerDeviceKind.mouse, - ); - + await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse); await pumpAndSettle(); } - Future openEmojiPicker() async { - await tapButton(find.byType(AddEmojiButton)); - } + Future openEmojiPicker() async => + tapButton(find.byType(AddEmojiButton)); Future tapDateCellInRowDetailPage() async { final findDateCell = find.byType(EditableDateCell); @@ -630,25 +590,12 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); } - Future duplicateRowInRowDetailPage() async { - final duplicateButton = find.byType(RowDetailPageDuplicateButton); - await tapButton(duplicateButton); - } - - Future deleteRowInRowDetailPage() async { - final deleteButton = find.byType(RowDetailPageDeleteButton); - await tapButton(deleteButton); - } - Future hoverOnFieldInRowDetail({required int index}) async { final fieldButtons = find.byType(FieldCellButton); final button = find .descendant(of: find.byType(RowDetailPage), matching: fieldButtons) .at(index); - return startGesture( - getCenter(button), - kind: PointerDeviceKind.mouse, - ); + return startGesture(getCenter(button), kind: PointerDeviceKind.mouse); } Future reorderFieldInRowDetail({required double offset}) async { @@ -657,11 +604,7 @@ extension AppFlowyDatabaseTest on WidgetTester { (widget) => widget is ReorderableDragStartListener && widget.enabled, ) .first; - await drag( - thumb, - Offset(0, offset), - kind: PointerDeviceKind.mouse, - ); + await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse); await pumpAndSettle(); } @@ -681,8 +624,7 @@ extension AppFlowyDatabaseTest on WidgetTester { Future tapDeletePropertyInFieldEditor() async { final deleteButton = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && widget.action == FieldAction.delete, + (w) => w is FieldActionCell && w.action == FieldAction.delete, ); await tapButton(deleteButton); @@ -693,11 +635,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(confirmButton); } - Future scrollGridByOffset(Offset offset) async { - await drag(find.byType(GridPage), offset); - await pumpAndSettle(); - } - Future scrollRowDetailByOffset(Offset offset) async { await drag(find.byType(RowDetailPage), offset); await pumpAndSettle(); @@ -756,8 +693,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapGridFieldWithName] first. Future tapDeletePropertyButton() async { final field = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && widget.action == FieldAction.delete, + (w) => w is FieldActionCell && w.action == FieldAction.delete, ); await tapButton(field); } @@ -765,9 +701,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// A SimpleDialog must be shown first, e.g. when deleting a field. Future tapDialogOkButton() async { final field = find.byWidgetPredicate( - (widget) => - widget is PrimaryTextButton && - widget.label == LocaleKeys.button_ok.tr(), + (w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(), ); await tapButton(field); } @@ -775,8 +709,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapGridFieldWithName] first. Future tapDuplicatePropertyButton() async { final field = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && widget.action == FieldAction.duplicate, + (w) => w is FieldActionCell && w.action == FieldAction.duplicate, ); await tapButton(field); } @@ -798,45 +731,34 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapGridFieldWithName] first. Future tapHidePropertyButton() async { final field = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && - widget.action == FieldAction.toggleVisibility, + (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility, ); await tapButton(field); } Future tapHidePropertyButtonInFieldEditor() async { final button = find.byWidgetPredicate( - (widget) => - widget is FieldActionCell && - widget.action == FieldAction.toggleVisibility, + (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility, ); await tapButton(button); } - Future tapRowDetailPageRowActionButton() async { - await tapButton(find.byType(RowActionButton)); - } + Future tapRowDetailPageRowActionButton() async => + tapButton(find.byType(RowActionButton)); - Future tapRowDetailPageCreatePropertyButton() async { - await tapButton(find.byType(CreateRowFieldButton)); - } + Future tapRowDetailPageCreatePropertyButton() async => + tapButton(find.byType(CreateRowFieldButton)); - Future tapRowDetailPageDeleteRowButton() async { - await tapButton(find.byType(RowDetailPageDeleteButton)); - } + Future tapRowDetailPageDeleteRowButton() async => + tapButton(find.byType(RowDetailPageDeleteButton)); - Future tapRowDetailPageDuplicateRowButton() async { - await tapButton(find.byType(RowDetailPageDuplicateButton)); - } + Future tapRowDetailPageDuplicateRowButton() async => + tapButton(find.byType(RowDetailPageDuplicateButton)); - Future tapSwitchFieldTypeButton() async { - await tapButton(find.byType(SwitchFieldButton)); - } + Future tapSwitchFieldTypeButton() async => + tapButton(find.byType(SwitchFieldButton)); - Future tapEscButton() async { - await sendKeyEvent(LogicalKeyboardKey.escape); - } + Future tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape); /// Must call [tapSwitchFieldTypeButton] first. Future selectFieldType(FieldType fieldType) async { @@ -851,15 +773,13 @@ extension AppFlowyDatabaseTest on WidgetTester { } // Use in edit mode of FieldEditor - void expectEmptyTypeOptionEditor() { - expect( - find.descendant( - of: find.byType(FieldTypeOptionEditor), - matching: find.byType(TypeOptionSeparator), - ), - findsNothing, - ); - } + void expectEmptyTypeOptionEditor() => expect( + find.descendant( + of: find.byType(FieldTypeOptionEditor), + matching: find.byType(TypeOptionSeparator), + ), + findsNothing, + ); /// Each field has its own cell, so we can find the corresponding cell by /// the field type after create a new field. @@ -868,10 +788,6 @@ extension AppFlowyDatabaseTest on WidgetTester { expect(finder, findsWidgets); } - Future assertNumberOfFieldsInGridPage(int num) async { - expect(find.byType(GridFieldCell), findsNWidgets(num)); - } - Future assertNumberOfRowsInGridPage(int num) async { expect( find.byType(GridRow, skipOffstage: false), @@ -884,14 +800,11 @@ extension AppFlowyDatabaseTest on WidgetTester { } /// Check the field type of the [FieldCellButton] is the same as the name. - Future assertFieldTypeWithFieldName( - String name, - FieldType fieldType, - ) async { + Future assertFieldTypeWithFieldName(String name, FieldType type) async { final field = find.byWidgetPredicate( (widget) => widget is FieldCellButton && - widget.field.fieldType == fieldType && + widget.field.fieldType == type && widget.field.name == name, ); @@ -936,11 +849,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(const Duration(milliseconds: 200)); } - Future findFieldEditor(dynamic matcher) async { - final finder = find.byType(FieldEditor); - expect(finder, matcher); - } - Future findDateEditor(dynamic matcher) async { final finder = find.byType(DateCellEditor); expect(finder, matcher); @@ -994,41 +902,29 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(find.byType(SortButton)); } - Future tapCreateFilterByFieldType( - FieldType fieldType, - String title, - ) async { + Future tapCreateFilterByFieldType(FieldType type, String title) async { final findFilter = find.byWidgetPredicate( (widget) => widget is GridFilterPropertyCell && - widget.fieldInfo.fieldType == fieldType && + widget.fieldInfo.fieldType == type && widget.fieldInfo.name == title, ); - await tapButton(findFilter); } - Future tapFilterButtonInGrid(String filterName) async { + Future tapFilterButtonInGrid(String name) async { final findFilter = find.byType(FilterMenuItem); - final button = find.descendant( - of: findFilter, - matching: find.text(filterName), - ); - + final button = find.descendant(of: findFilter, matching: find.text(name)); await tapButton(button); } - Future tapCreateSortByFieldType( - FieldType fieldType, - String title, - ) async { + Future tapCreateSortByFieldType(FieldType type, String title) async { final findSort = find.byWidgetPredicate( (widget) => widget is GridSortPropertyCell && - widget.fieldInfo.fieldType == fieldType && + widget.fieldInfo.fieldType == type && widget.fieldInfo.name == title, ); - await tapButton(findSort); } @@ -1085,10 +981,7 @@ extension AppFlowyDatabaseTest on WidgetTester { of: fromSortItem, matching: find.byType(ReorderableDragStartListener), ); - await drag( - dragElement, - getCenter(toSortItem) - getCenter(fromSortItem), - ); + await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem)); await pumpAndSettle(const Duration(milliseconds: 200)); } @@ -1166,15 +1059,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(findCell); } - Future tapCheckedButtonOnCheckboxFilter() async { - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()), - ); - - await tapButton(button); - } - Future tapUnCheckedButtonOnCheckboxFilter() async { final button = find.descendant( of: find.byType(HoverButton), @@ -1193,15 +1077,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(button); } - Future tapUnCompletedButtonOnChecklistFilter() async { - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()), - ); - - await tapButton(button); - } - /// Should call [tapDatabaseSettingButton] first. Future tapViewPropertiesButton() async { final findSettingItem = find.byType(DatabaseSettingsList); @@ -1252,16 +1127,8 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(button); } - Future tapFirstDayOfWeek() async { - await tapButton(find.byType(FirstDayOfWeek)); - } - - Future tapFirstDayOfWeekStartFromSunday() async { - final finder = find.byWidgetPredicate( - (widget) => widget is StartFromButton && widget.dayIndex == 0, - ); - await tapButton(finder); - } + Future tapFirstDayOfWeek() async => + tapButton(find.byType(FirstDayOfWeek)); Future tapFirstDayOfWeekStartFromMonday() async { final finder = find.byWidgetPredicate( @@ -1277,20 +1144,14 @@ extension AppFlowyDatabaseTest on WidgetTester { void assertFirstDayOfWeekStartFromMonday() { final finder = find.byWidgetPredicate( - (widget) => - widget is StartFromButton && - widget.dayIndex == 1 && - widget.isSelected == true, + (w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true, ); expect(finder, findsOneWidget); } void assertFirstDayOfWeekStartFromSunday() { final finder = find.byWidgetPredicate( - (widget) => - widget is StartFromButton && - widget.dayIndex == 0 && - widget.isSelected == true, + (w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true, ); expect(finder, findsOneWidget); } @@ -1307,11 +1168,7 @@ extension AppFlowyDatabaseTest on WidgetTester { ), ) .first; - await scrollUntilVisible( - todayCell, - 300, - scrollable: scrollable, - ); + await scrollUntilVisible(todayCell, 300, scrollable: scrollable); await pumpAndSettle(const Duration(milliseconds: 300)); } @@ -1351,12 +1208,7 @@ extension AppFlowyDatabaseTest on WidgetTester { String? title, }) { final findDayCell = find.byWidgetPredicate( - (widget) => - widget is CalendarDayCard && - isSameDay( - widget.date, - date, - ), + (widget) => widget is CalendarDayCard && isSameDay(widget.date, date), ); Finder findEvents = find.descendant( of: findDayCell, @@ -1390,13 +1242,11 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(cards.at(index)); } - void assertEventEditorOpen() { - expect(find.byType(CalendarEventEditor), findsOneWidget); - } + void assertEventEditorOpen() => + expect(find.byType(CalendarEventEditor), findsOneWidget); - Future dismissEventEditor() async { - await simulateKeyEvent(LogicalKeyboardKey.escape); - } + Future dismissEventEditor() async => + simulateKeyEvent(LogicalKeyboardKey.escape); Future editEventTitle(String title) async { final textField = find.descendant( @@ -1507,10 +1357,9 @@ extension AppFlowyDatabaseTest on WidgetTester { matching: find.byType(TextField), ); if (isVisible) { - expect(textField, findsOneWidget); - } else { - expect(textField, findsNothing); + return expect(textField, findsOneWidget); } + expect(textField, findsNothing); } Future enterNewGroupName(String name, {required bool submit}) async { @@ -1612,21 +1461,14 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(okButton); } - void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) { - switch (layout) { - case DatabaseLayoutPB.Board: - expect(find.byType(BoardPage), findsOneWidget); - break; - case DatabaseLayoutPB.Calendar: - expect(find.byType(CalendarPage), findsOneWidget); - break; - case DatabaseLayoutPB.Grid: - expect(find.byType(GridPage), findsOneWidget); - break; - default: - throw Exception('Unknown database layout type: $layout'); - } - } + void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) { + DatabaseLayoutPB.Board => + expect(find.byType(BoardPage), findsOneWidget), + DatabaseLayoutPB.Calendar => + expect(find.byType(CalendarPage), findsOneWidget), + DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget), + _ => throw Exception('Unknown database layout type: $layout'), + }; Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { final findLayoutCell = find.byType(DatabaseViewLayoutCell); @@ -1634,11 +1476,7 @@ extension AppFlowyDatabaseTest on WidgetTester { (widget) => widget is FlowyText && widget.text == layout.layoutName, ); - final button = find.descendant( - of: findLayoutCell, - matching: findText, - ); - + final button = find.descendant(of: findLayoutCell, matching: findText); await tapButton(button); } @@ -1660,8 +1498,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton( find.byWidgetPredicate( - (widget) => - widget is NumberFormatCell && widget.format == NumberFormatPB.USD, + (w) => w is NumberFormatCell && w.format == NumberFormatPB.USD, ), ); } @@ -1675,8 +1512,7 @@ extension AppFlowyDatabaseTest on WidgetTester { String fieldName, ) async { final field = find.byWidgetPredicate( - (widget) => - widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName, + (w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName, ); final toggleVisibilityButton = find.descendant(of: field, matching: find.byType(FlowyIconButton)); @@ -1684,18 +1520,12 @@ extension AppFlowyDatabaseTest on WidgetTester { } } -Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) { - switch (layout) { - case DatabaseLayoutPB.Board: - return find.byType(BoardPage); - case DatabaseLayoutPB.Calendar: - return find.byType(CalendarPage); - case DatabaseLayoutPB.Grid: - return find.byType(GridPage); - default: - throw Exception('Unknown database layout type: $layout'); - } -} +Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) { + DatabaseLayoutPB.Board => find.byType(BoardPage), + DatabaseLayoutPB.Calendar => find.byType(CalendarPage), + DatabaseLayoutPB.Grid => find.byType(GridPage), + _ => throw Exception('Unknown database layout type: $layout'), + }; Finder finderForFieldType(FieldType fieldType) { switch (fieldType) { diff --git a/frontend/appflowy_flutter/integration_test/shared/dir.dart b/frontend/appflowy_flutter/integration_test/shared/dir.dart index 9dc00a57c9..56c6f302f0 100644 --- a/frontend/appflowy_flutter/integration_test/shared/dir.dart +++ b/frontend/appflowy_flutter/integration_test/shared/dir.dart @@ -1,6 +1,7 @@ import 'dart:io'; -import 'package:path/path.dart' as p; + import 'package:archive/archive.dart'; +import 'package:path/path.dart' as p; Future deleteDirectoriesWithSameBaseNameAsPrefix( String path, diff --git a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart index 2d8f9ff766..0bdcf06367 100644 --- a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart @@ -30,11 +30,8 @@ class EditorOperations { final WidgetTester tester; - EditorState getCurrentEditorState() { - return tester - .widget(find.byType(AppFlowyEditor)) - .editorState; - } + EditorState getCurrentEditorState() => + tester.widget(find.byType(AppFlowyEditor)).editorState; /// Tap the line of editor at [index] Future tapLineOfEditorAt(int index) async { @@ -144,16 +141,8 @@ class EditorOperations { ); } - Future switchNetworkImageCover(String imageUrl) async { - final image = find.byWidgetPredicate( - (widget) => widget is ImageGridItem, - ); - await tester.tapButton(image); - } - - Future tapOnRemoveCover() async { - await tester.tapButton(find.byType(DeleteCoverButton)); - } + Future tapOnRemoveCover() async => + tester.tapButton(find.byType(DeleteCoverButton)); /// A cover must be present in the document to function properly since this /// catches all cover types collectively diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index cfca2e595b..5f831f3d28 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; @@ -12,7 +14,6 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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_test/flutter_test.dart'; import 'util.dart'; @@ -89,18 +90,6 @@ extension Expectation on WidgetTester { expect(exportSuccess, findsOneWidget); } - /// Expect to see the add button and icon button in the cover toolbar - void expectToSeePluginAddCoverAndIconButton() { - final addCover = find.textContaining( - LocaleKeys.document_plugins_cover_addCover.tr(), - ); - final addIcon = find.textContaining( - LocaleKeys.document_plugins_cover_addIcon.tr(), - ); - expect(addCover, findsOneWidget); - expect(addIcon, findsOneWidget); - } - /// Expect to see the document header toolbar empty void expectToSeeEmptyDocumentHeaderToolbar() { final addCover = find.textContaining( @@ -153,14 +142,6 @@ extension Expectation on WidgetTester { expect(findRemoveIcon, findsOneWidget); } - /// Expect to see the user name on the home page - void expectToSeeUserName(String name) { - final userName = find.byWidgetPredicate( - (widget) => widget is FlowyText && widget.text == name, - ); - expect(userName, findsOneWidget); - } - /// Expect to see a text void expectToSeeText(String text) { Finder textWidget = find.textContaining(text, findRichText: true); @@ -178,26 +159,23 @@ extension Expectation on WidgetTester { ViewLayoutPB layout = ViewLayoutPB.Document, String? parentName, ViewLayoutPB parentLayout = ViewLayoutPB.Document, - }) { - return find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite && - widget.view.name == name && - widget.view.layout == layout, - skipOffstage: false, - ); - } + }) => + find.byWidgetPredicate( + (widget) => + widget is SingleInnerViewItem && + widget.view.isFavorite && + widget.categoryType == FolderCategoryType.favorite && + widget.view.name == name && + widget.view.layout == layout, + skipOffstage: false, + ); - Finder findAllFavoritePages() { - return find.byWidgetPredicate( - (widget) => - widget is SingleInnerViewItem && - widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, - ); - } + Finder findAllFavoritePages() => find.byWidgetPredicate( + (widget) => + widget is SingleInnerViewItem && + widget.view.isFavorite && + widget.categoryType == FolderCategoryType.favorite, + ); Finder findPageName( String name, { diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart index 6c4c93eaea..32a0c255d8 100644 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart @@ -11,9 +11,7 @@ class MockFilePicker implements FilePickerService { final List mockPaths; @override - Future getDirectoryPath({String? title}) { - return Future.value(mockPath); - } + Future getDirectoryPath({String? title}) => Future.value(mockPath); @override Future saveFile({ @@ -23,9 +21,8 @@ class MockFilePicker implements FilePickerService { FileType type = FileType.any, List? allowedExtensions, bool lockParentWindow = false, - }) { - return Future.value(mockPath); - } + }) => + Future.value(mockPath); @override Future pickFiles({ @@ -42,34 +39,21 @@ class MockFilePicker implements FilePickerService { }) { final platformFiles = mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList(); - return Future.value( - FilePickerResult( - platformFiles, - ), - ); + return Future.value(FilePickerResult(platformFiles)); } } -Future mockGetDirectoryPath( - String path, -) async { +Future mockGetDirectoryPath(String path) async { getIt.unregister(); getIt.registerFactory( - () => MockFilePicker( - mockPath: path, - ), + () => MockFilePicker(mockPath: path), ); - return; } -Future mockSaveFilePath( - String path, -) async { +Future mockSaveFilePath(String path) async { getIt.unregister(); getIt.registerFactory( - () => MockFilePicker( - mockPath: path, - ), + () => MockFilePicker(mockPath: path), ); return path; } @@ -77,9 +61,7 @@ Future mockSaveFilePath( List mockPickFilePaths({required List paths}) { getIt.unregister(); getIt.registerFactory( - () => MockFilePicker( - mockPaths: paths, - ), + () => MockFilePicker(mockPaths: paths), ); return paths; } diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart index 2941bcb49b..78445a2f4e 100644 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart @@ -1,10 +1,11 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; -import 'package:mocktail/mocktail.dart'; -import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; -import 'package:http/http.dart' as http; import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; class MyMockClient extends Mock implements http.Client { @override @@ -52,7 +53,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository { final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); final response = await client.send(request); - var previousSyntax = ''; + String previousSyntax = ''; if (response.statusCode == 200) { await for (final chunk in response.stream .transform(const Utf8Decoder()) @@ -76,6 +77,5 @@ class MockOpenAIRepository extends HttpOpenAIRepository { } } } - return; } } diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart index 05c8b5e4b3..b0281ab4b4 100644 --- a/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart @@ -27,9 +27,7 @@ class MockUrlLauncher extends Fake bool launchCalled = false; // ignore: use_setters_to_change_properties - void setCanLaunchExpectations(String url) { - this.url = url; - } + void setCanLaunchExpectations(String url) => this.url = url; void setLaunchExpectations({ required String url, @@ -53,10 +51,7 @@ class MockUrlLauncher extends Fake this.webOnlyWindowName = webOnlyWindowName; } - // ignore: use_setters_to_change_properties - void setResponse(bool response) { - this.response = response; - } + void setResponse(bool response) => this.response = response; @override LinkDelegate? get linkDelegate => null; @@ -104,7 +99,5 @@ class MockUrlLauncher extends Fake } @override - Future closeWebView() async { - closeWebViewCalled = true; - } + Future closeWebView() async => closeWebViewCalled = true; } diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index 8f48aa3694..cd6564b7cb 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -1,13 +1,17 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../desktop/board/board_hide_groups_test.dart'; + import 'base.dart'; extension AppFlowySettings on WidgetTester { @@ -31,14 +35,6 @@ extension AppFlowySettings on WidgetTester { return; } - Future expectNoSettingsPage(SettingsPage page) async { - final button = find.byWidgetPredicate( - (widget) => widget is SettingsMenuElement && widget.page == page, - ); - expect(button, findsNothing); - return; - } - /// Restore the AppFlowy data storage location Future restoreLocation() async { final button = @@ -48,13 +44,6 @@ extension AppFlowySettings on WidgetTester { return; } - Future tapOpenFolderButton() async { - final button = find.text(LocaleKeys.settings_files_open.tr()); - expect(button, findsOneWidget); - await tapButton(button); - return; - } - Future tapCustomLocationButton() async { final button = find.byTooltip( LocaleKeys.settings_files_changeLocationTooltips.tr(), @@ -66,12 +55,22 @@ extension AppFlowySettings on WidgetTester { /// Enter user name Future enterUserName(String name) async { - final uni = find.byType(UserNameInput); - expect(uni, findsOneWidget); - await tap(uni); - await enterText(uni, name); - await wait(300); // - await testTextInput.receiveAction(TextInputAction.done); + // Enable editing username + final editUsernameFinder = find.descendant( + of: find.byType(UserProfileSetting), + matching: find.byFlowySvg(FlowySvgs.edit_s), + ); + await tap(editUsernameFinder); + await pumpAndSettle(); + + final userNameFinder = find.descendant( + of: find.byType(UserProfileSetting), + matching: find.byType(FlowyTextField), + ); + await enterText(userNameFinder, name); + await pumpAndSettle(); + + await tap(find.text(LocaleKeys.button_save.tr())); await pumpAndSettle(); } diff --git a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart index 259ab09745..afe850f88f 100644 --- a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart @@ -1,18 +1,10 @@ -import 'dart:typed_data'; - import 'package:appflowy/core/notification/notification_helper.dart'; import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; // This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value const String _source = 'Document'; -typedef DocumentNotificationCallback = void Function( - DocumentNotification, - FlowyResult, -); - class DocumentNotificationParser extends NotificationParser { DocumentNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart index e7304ac14b..a5b99484bc 100644 --- a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart @@ -12,12 +12,6 @@ import 'notification_helper.dart'; // This value should be the same as the FOLDER_OBSERVABLE_SOURCE value const String _source = 'Workspace'; -// Folder -typedef FolderNotificationCallback = void Function( - FolderNotification, - FlowyResult, -); - class FolderNotificationParser extends NotificationParser { FolderNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart index 38676e384c..d5425a042d 100644 --- a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart @@ -12,12 +12,6 @@ import 'notification_helper.dart'; // This value should be the same as the DATABASE_OBSERVABLE_SOURCE value const String _source = 'Database'; -// DatabasePB -typedef DatabaseNotificationCallback = void Function( - DatabaseNotification, - FlowyResult, -); - class DatabaseNotificationParser extends NotificationParser { DatabaseNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/search_notification.dart b/frontend/appflowy_flutter/lib/core/notification/search_notification.dart index 0e11f1c9aa..18f9b218f1 100644 --- a/frontend/appflowy_flutter/lib/core/notification/search_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/search_notification.dart @@ -13,11 +13,6 @@ import 'notification_helper.dart'; // This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE) const _source = 'Search'; -typedef SearchNotificationCallback = void Function( - SearchNotification, - FlowyResult, -); - class SearchNotificationParser extends NotificationParser { SearchNotificationParser({ diff --git a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart index 36c7638df5..c9582c3dda 100644 --- a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart @@ -1,23 +1,11 @@ -import 'dart:async'; -import 'dart:typed_data'; - import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; // This value should be the same as the USER_OBSERVABLE_SOURCE value const String _source = 'User'; -// User -typedef UserNotificationCallback = void Function( - UserNotification, - FlowyResult, -); - class UserNotificationParser extends NotificationParser { UserNotificationParser({ @@ -29,26 +17,3 @@ class UserNotificationParser errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } - -typedef UserNotificationHandler = Function( - UserNotification ty, - FlowyResult result, -); - -class UserNotificationListener { - UserNotificationListener({ - required String objectId, - required UserNotificationHandler handler, - }) : _parser = UserNotificationParser(id: objectId, callback: handler) { - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - UserNotificationParser? _parser; - StreamSubscription? _subscription; - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart deleted file mode 100644 index aed96d6944..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/box_container.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class FlowyBoxContainer extends StatelessWidget { - const FlowyBoxContainer({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 8.0, - ), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.onSecondary, - ), - borderRadius: BorderRadius.circular(8.0), - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index c6782c0ce1..86a2c0dc51 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -6,14 +8,13 @@ import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bott import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_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'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.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:go_router/go_router.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart deleted file mode 100644 index 13ba942f49..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; - -class FlowyEmojiPickerI18n extends EmojiPickerI18n { - @override - String get activity => LocaleKeys.emoji_categories_activities.tr(); - - @override - String get flags => LocaleKeys.emoji_categories_flags.tr(); - - @override - String get foods => LocaleKeys.emoji_categories_food.tr(); - - @override - String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr(); - - @override - String get nature => LocaleKeys.emoji_categories_nature.tr(); - - @override - String get objects => LocaleKeys.emoji_categories_objects.tr(); - - @override - String get people => LocaleKeys.emoji_categories_smileys.tr(); - - @override - String get places => LocaleKeys.emoji_categories_places.tr(); - - @override - String get search => LocaleKeys.emoji_search.tr(); - - @override - String get symbols => LocaleKeys.emoji_categories_symbols.tr(); - - @override - String get searchHintText => LocaleKeys.emoji_search.tr(); - - @override - String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr(); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart index 791423901f..c7d1169b4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart @@ -6,11 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'cell_controller.dart'; -abstract class IGridCellDataConfig { - // The cell data will reload if it receives the field's change notification. - bool get reloadOnFieldChanged; -} - abstract class CellDataParser { T? parserData(List data); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart index 88e9fc4f77..48785fc87f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart @@ -31,8 +31,6 @@ typedef OnNumOfRowsChanged = void Function( ChangedReason reason, ); -typedef OnError = void Function(FlowyError); - @freezed class LoadingState with _$LoadingState { const factory LoadingState.idle() = _Idle; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index 1b9340d93c..7fa5c81d3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -1,13 +1,15 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; + +import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/application/setting/setting_listener.dart'; import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/domain/field_listener.dart'; import 'package:appflowy/plugins/database/domain/field_settings_listener.dart'; import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; import 'package:appflowy/plugins/database/domain/filter_listener.dart'; import 'package:appflowy/plugins/database/domain/filter_service.dart'; -import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/application/setting/setting_listener.dart'; import 'package:appflowy/plugins/database/domain/sort_listener.dart'; import 'package:appflowy/plugins/database/domain/sort_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; @@ -17,9 +19,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import '../setting/setting_service.dart'; + import 'field_info.dart'; class _GridFieldNotifier extends ChangeNotifier { @@ -73,7 +75,7 @@ typedef OnReceiveField = void Function(FieldInfo); typedef OnReceiveFields = void Function(List); typedef OnReceiveFilters = void Function(List); typedef OnReceiveSorts = void Function(List); -typedef OnReceiveFieldSettings = void Function(List); + class FieldController { FieldController({required this.viewId}) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart index b49b3a80df..f93a7a3d02 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart @@ -4,13 +4,7 @@ abstract class TypeOptionParser { T fromBuffer(List buffer); } -class RichTextTypeOptionDataParser - extends TypeOptionParser { - @override - RichTextTypeOptionPB fromBuffer(List buffer) { - return RichTextTypeOptionPB.fromBuffer(buffer); - } -} + class NumberTypeOptionDataParser extends TypeOptionParser { @override @@ -19,21 +13,6 @@ class NumberTypeOptionDataParser extends TypeOptionParser { } } -class CheckboxTypeOptionDataParser - extends TypeOptionParser { - @override - CheckboxTypeOptionPB fromBuffer(List buffer) { - return CheckboxTypeOptionPB.fromBuffer(buffer); - } -} - -class URLTypeOptionDataParser extends TypeOptionParser { - @override - URLTypeOptionPB fromBuffer(List buffer) { - return URLTypeOptionPB.fromBuffer(buffer); - } -} - class DateTypeOptionDataParser extends TypeOptionParser { @override DateTypeOptionPB fromBuffer(List buffer) { @@ -65,14 +44,6 @@ class MultiSelectTypeOptionDataParser } } -class ChecklistTypeOptionDataParser - extends TypeOptionParser { - @override - ChecklistTypeOptionPB fromBuffer(List buffer) { - return ChecklistTypeOptionPB.fromBuffer(buffer); - } -} - class RelationTypeOptionDataParser extends TypeOptionParser { @override diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart index 7e0700e263..4fa7a15672 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart @@ -9,8 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:protobuf/protobuf.dart'; -typedef OnGroupError = void Function(FlowyError); - abstract class GroupControllerDelegate { bool hasGroup(String groupId); void removeRow(GroupPB group, RowId rowId); diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart deleted file mode 100644 index ea2e1b9314..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/toolbar/board_setting_bloc.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'board_setting_bloc.freezed.dart'; - -class BoardSettingBloc extends Bloc { - BoardSettingBloc({required this.viewId}) - : super(BoardSettingState.initial()) { - on( - (event, emit) async { - event.when( - performAction: (action) { - emit(state.copyWith(selectedAction: action)); - }, - ); - }, - ); - } - - final String viewId; -} - -@freezed -class BoardSettingEvent with _$BoardSettingEvent { - const factory BoardSettingEvent.performAction(BoardSettingAction action) = - _PerformAction; -} - -@freezed -class BoardSettingState with _$BoardSettingState { - const factory BoardSettingState({ - required BoardSettingAction? selectedAction, - }) = _BoardSettingState; - - factory BoardSettingState.initial() => const BoardSettingState( - selectedAction: null, - ); -} - -enum BoardSettingAction { - properties, - groups, -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart index 4a2674f1c5..ecfb4bfff8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart @@ -456,16 +456,6 @@ class CalendarState with _$CalendarState { ); } -class CalendarEditingRow { - CalendarEditingRow({ - required this.row, - required this.index, - }); - - RowPB row; - int? index; -} - @freezed class CalendarDayEvent with _$CalendarDayEvent { const factory CalendarDayEvent({ diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart index 8ea42d2d3b..c1288b157d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart @@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart'; part 'calendar_setting_bloc.freezed.dart'; -typedef DayOfWeek = int; - class CalendarSettingBloc extends Bloc { CalendarSettingBloc({required DatabaseController databaseController}) diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 9ee1f58016..c38c7647ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../application/row/row_controller.dart'; import '../../widgets/row/row_detail.dart'; + import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; @@ -265,6 +267,7 @@ class _CalendarPageState extends State { fillColor: Colors.transparent, fontWeight: FontWeight.w400, fontSize: 10, + fontColor: AFThemeExtension.of(context).textColor, tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), hoverColor: AFThemeExtension.of(context).lightGreyHover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart deleted file mode 100644 index 8144904cad..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class DatabaseBackendService { - static Future, FlowyError>> - getAllDatabases() { - return DatabaseEventGetDatabases().send().then((result) { - return result.fold( - (l) => FlowyResult.success(l.items), - (r) => FlowyResult.failure(r), - ); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart index 212bce80c3..8720c18866 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart @@ -8,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; -typedef GroupConfigurationUpdateValue - = FlowyResult, FlowyError>; typedef GroupUpdateValue = FlowyResult; typedef GroupByNewFieldValue = FlowyResult, FlowyError>; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart index 92d9360106..b6817fc848 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart @@ -1,4 +1,5 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; @@ -8,14 +9,9 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; -import "package:appflowy/generated/locale_keys.g.dart"; -import 'package:easy_localization/easy_localization.dart'; class MobileGridRow extends StatefulWidget { const MobileGridRow({ @@ -90,26 +86,6 @@ class _MobileGridRowState extends State { } } -class InsertRowButton extends StatelessWidget { - const InsertRowButton({super.key}); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - tooltipText: LocaleKeys.tooltip_addNewRow.tr(), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - width: 20, - height: 30, - onPressed: () => context.read().add(const RowEvent.createRow()), - iconPadding: const EdgeInsets.all(3), - icon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).colorScheme.tertiary, - ), - ); - } -} - class RowContent extends StatelessWidget { const RowContent({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart index faf33c61ec..7d743702fc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart @@ -23,27 +23,12 @@ Map bindKeys(List keys) { return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; } -Map> bindActions() { - return { - KeyboardKeyIdent: KeyboardBindingAction(), - }; -} - class KeyboardKeyIdent extends Intent { const KeyboardKeyIdent(this.key); final KeyboardKey key; } -class KeyboardBindingAction extends Action { - KeyboardBindingAction(); - - @override - void invoke(covariant KeyboardKeyIdent intent) { - // print(intent); - } -} - class LoggingActionDispatcher extends ActionDispatcher { @override Object? invokeAction( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart index 129e17ca48..56045d57d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart @@ -1,6 +1,7 @@ -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; + enum AccessoryType { edit, more, @@ -11,10 +12,6 @@ abstract mixin class CardAccessory implements Widget { void onTap(BuildContext context) {} } -typedef CardAccessoryBuilder = List Function( - BuildContext buildContext, -); - class CardAccessoryContainer extends StatelessWidget { const CardAccessoryContainer({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart deleted file mode 100755 index 0e74073e14..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_decoration.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class CellDecoration { - static BoxDecoration box({required Color color}) { - return BoxDecoration( - border: Border.all(color: Colors.black26, width: 0.2), - color: color, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart index 582e5fccd0..3fc2131f24 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart @@ -96,21 +96,3 @@ class GridCellCopyAction extends Action { } } } - -class GridCellPasteIntent extends Intent { - const GridCellPasteIntent(); -} - -class GridCellPasteAction extends Action { - GridCellPasteAction({required this.child}); - - final CellShortcuts child; - - @override - void invoke(covariant GridCellPasteIntent intent) { - final callback = child.shortcutHandlers[CellKeyboardKey.onInsert]; - if (callback != null) { - callback(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 82ae9b010f..21ff5da21d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -1,5 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart'; @@ -8,7 +11,6 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -19,10 +21,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/em import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -typedef OnSubmittedEmoji = void Function(String emoji); const _kBannerActionHeight = 40.0; class RowBanner extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart deleted file mode 100644 index 15e2610bdc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action_button.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; -import 'package:flutter/material.dart'; - -class OptionActionList extends StatelessWidget { - const OptionActionList({ - super.key, - required this.blockComponentContext, - required this.blockComponentState, - required this.actions, - required this.editorState, - }); - - final BlockComponentContext blockComponentContext; - final BlockComponentActionState blockComponentState; - final List actions; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final popoverActions = actions.map((e) { - if (e == OptionAction.divider) { - return DividerOptionAction(); - } else if (e == OptionAction.color) { - return ColorOptionAction( - editorState: editorState, - ); - } else if (e == OptionAction.depth) { - return DepthOptionAction( - editorState: editorState, - ); - } else { - return OptionActionWrapper(e); - } - }).toList(); - - return PopoverActionList( - popoverMutex: PopoverMutex(), - direction: PopoverDirection.leftWithCenterAligned, - actions: popoverActions, - onPopupBuilder: () => blockComponentState.alwaysShowActions = true, - onClosed: () { - editorState.selectionType = null; - editorState.selection = null; - blockComponentState.alwaysShowActions = false; - }, - onSelected: (action, controller) { - if (action is OptionActionWrapper) { - _onSelectAction(action.inner); - controller.close(); - } - }, - buildChild: (controller) => OptionActionButton( - onTap: () { - controller.show(); - - // update selection - _updateBlockSelection(); - }, - ), - ); - } - - void _updateBlockSelection() { - final startNode = blockComponentContext.node; - var endNode = startNode; - while (endNode.children.isNotEmpty) { - endNode = endNode.children.last; - } - - final start = Position(path: startNode.path); - final end = endNode.selectable?.end() ?? - Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ); - - editorState.selectionType = SelectionType.block; - editorState.selection = Selection( - start: start, - end: end, - ); - } - - void _onSelectAction(OptionAction action) { - final node = blockComponentContext.node; - final transaction = editorState.transaction; - switch (action) { - case OptionAction.delete: - transaction.deleteNode(node); - break; - case OptionAction.duplicate: - transaction.insertNode( - node.path.next, - node.copyWith(), - ); - break; - case OptionAction.turnInto: - break; - case OptionAction.moveUp: - transaction.moveNode(node.path.previous, node); - break; - case OptionAction.moveDown: - transaction.moveNode(node.path.next.next, node); - break; - case OptionAction.align: - case OptionAction.color: - case OptionAction.divider: - case OptionAction.depth: - throw UnimplementedError(); - } - editorState.apply(transaction); - } -} - -class OptionActionButton extends StatelessWidget { - const OptionActionButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Align( - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: IgnoreParentGestureWidget( - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.deferToChild, - child: FlowySvg( - FlowySvgs.drag_element_s, - size: const Size.square(24.0), - color: Theme.of(context).iconTheme.color, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 37753b7724..aac0713a7d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -1,19 +1,8 @@ -import 'dart:io'; import 'dart:ui'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:appflowy_editor/appflowy_editor.dart'; const String kLocalImagesKey = 'local_images'; @@ -22,302 +11,6 @@ List get builtInAssetImages => [ "assets/images/app_flowy_abstract_cover_2.jpg", ]; -class ChangeCoverPopover extends StatefulWidget { - const ChangeCoverPopover({ - super.key, - required this.editorState, - required this.node, - required this.onCoverChanged, - }); - - final EditorState editorState; - final Node node; - final Function( - CoverType selectionType, - String selection, - ) onCoverChanged; - - @override - State createState() => _ChangeCoverPopoverState(); -} - -class _ChangeCoverPopoverState extends State { - bool isAddingImage = false; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ChangeCoverPopoverBloc( - editorState: widget.editorState, - node: widget.node, - )..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()), - child: BlocConsumer( - listener: (context, state) { - if (state is Loaded && state.selectLatestImage) { - widget.onCoverChanged( - CoverType.file, - state.imageNames.last, - ); - } - }, - builder: (context, state) { - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: isAddingImage - ? CoverImagePicker( - onBackPressed: () => setState(() { - isAddingImage = false; - }), - onFileSubmit: (_) { - context.read().add( - const ChangeCoverPopoverEvent - .fetchPickedImagePaths( - selectLatestImage: true, - ), - ); - - setState(() => isAddingImage = false); - }, - ) - : _buildCoverSelection(), - ), - ); - }, - ), - ); - } - - Widget _buildCoverSelection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.document_plugins_cover_colors.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - const VSpace(10), - _buildColorPickerList(), - const VSpace(10), - _buildImageHeader(), - const VSpace(10), - _buildFileImagePicker(), - const VSpace(10), - FlowyText.semibold( - LocaleKeys.document_plugins_cover_abstract.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - const VSpace(10), - _buildAbstractImagePicker(), - ], - ); - } - - Widget _buildImageHeader() { - return BlocBuilder( - builder: (context, state) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FlowyText.semibold( - LocaleKeys.document_plugins_cover_images.tr(), - color: Theme.of(context).colorScheme.tertiary, - ), - FlowyTextButton( - fillColor: Theme.of(context).cardColor, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - LocaleKeys.document_plugins_cover_clearAll.tr(), - fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async { - final hasFileImageCover = CoverType.fromString( - widget.node.attributes[DocumentHeaderBlockKeys.coverType], - ) == - CoverType.file; - final changeCoverBloc = context.read(); - if (hasFileImageCover) { - await showDialog( - context: context, - builder: (context) { - return DeleteImageAlertDialog( - onSubmit: () { - changeCoverBloc.add( - const ChangeCoverPopoverEvent.clearAllImages(), - ); - Navigator.pop(context); - }, - ); - }, - ); - } else { - context - .read() - .add(const ChangeCoverPopoverEvent.clearAllImages()); - } - }, - mainAxisAlignment: MainAxisAlignment.end, - ), - ], - ); - }, - ); - } - - Widget _buildAbstractImagePicker() { - return GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1 / 0.65, - crossAxisSpacing: 7, - mainAxisSpacing: 7, - ), - itemCount: builtInAssetImages.length, - itemBuilder: (BuildContext ctx, index) { - return InkWell( - onTap: () { - widget.onCoverChanged( - CoverType.asset, - builtInAssetImages[index], - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(builtInAssetImages[index]), - fit: BoxFit.cover, - ), - borderRadius: Corners.s8Border, - ), - ), - ); - }, - ); - } - - Widget _buildColorPickerList() { - final theme = Theme.of(context); - return CoverColorPicker( - pickerBackgroundColor: theme.cardColor, - pickerItemHoverColor: theme.hoverColor, - selectedBackgroundColorHex: - widget.node.attributes[DocumentHeaderBlockKeys.coverType] == - CoverType.color.toString() - ? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails] - : 'ffffff', - backgroundColorOptions: - _generateBackgroundColorOptions(widget.editorState), - onSubmittedBackgroundColorHex: (color) { - widget.onCoverChanged(CoverType.color, color); - setState(() {}); - }, - ); - } - - Widget _buildFileImagePicker() { - return BlocBuilder( - builder: (context, state) { - if (state is Loaded) { - final List images = state.imageNames; - return GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1 / 0.65, - crossAxisSpacing: 7, - mainAxisSpacing: 7, - ), - itemCount: images.length + 1, - itemBuilder: (BuildContext ctx, index) { - if (index == 0) { - return NewCustomCoverButton( - onPressed: () => setState( - () => isAddingImage = true, - ), - ); - } - return ImageGridItem( - onImageSelect: () { - widget.onCoverChanged( - CoverType.file, - images[index - 1], - ); - }, - onImageDelete: () async { - final changeCoverBloc = - context.read(); - final deletingCurrentCover = widget.node - .attributes[DocumentHeaderBlockKeys.coverDetails] == - images[index - 1]; - if (deletingCurrentCover) { - await showDialog( - context: context, - builder: (context) { - return DeleteImageAlertDialog( - onSubmit: () { - changeCoverBloc.add( - ChangeCoverPopoverEvent.deleteImage( - images[index - 1], - ), - ); - Navigator.pop(context); - }, - ); - }, - ); - } else { - changeCoverBloc.add(DeleteImage(images[index - 1])); - } - }, - imagePath: images[index - 1], - ); - }, - ); - } - - return const SizedBox.shrink(); - }, - ); - } - - List _generateBackgroundColorOptions(EditorState editorState) { - return FlowyTint.values - .map( - (t) => ColorOption( - colorHex: t.color(context).toHex(), - name: t.tintName(AppFlowyEditorL10n.current), - ), - ) - .toList(); - } -} - -@visibleForTesting -class NewCustomCoverButton extends StatelessWidget { - const NewCustomCoverButton({super.key, required this.onPressed}); - - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: Corners.s8Border, - ), - child: FlowyIconButton( - icon: Icon( - Icons.add, - color: Theme.of(context).colorScheme.primary, - ), - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.15), - onPressed: onPressed, - ), - ); - } -} - class ColorOption { const ColorOption({ required this.colorHex, @@ -398,122 +91,6 @@ class _CoverColorPickerState extends State { } } -class DeleteImageAlertDialog extends StatelessWidget { - const DeleteImageAlertDialog({ - super.key, - required this.onSubmit, - }); - - final Function() onSubmit; - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: FlowyText.semibold( - "Image is used in cover", - fontSize: 20, - color: Theme.of(context).colorScheme.tertiary, - ), - content: Container( - constraints: const BoxConstraints(minHeight: 100), - padding: const EdgeInsets.symmetric( - vertical: 20, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text(LocaleKeys.document_plugins_cover_coverRemoveAlert).tr(), - const SizedBox( - height: 4, - ), - const Text( - LocaleKeys.document_plugins_cover_alertDialogConfirmation, - ).tr(), - ], - ), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 20.0, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text(LocaleKeys.button_cancel).tr(), - ), - TextButton( - onPressed: onSubmit, - child: const Text(LocaleKeys.button_ok).tr(), - ), - ], - ); - } -} - -class ImageGridItem extends StatefulWidget { - const ImageGridItem({ - super.key, - required this.onImageSelect, - required this.onImageDelete, - required this.imagePath, - }); - - final Function() onImageSelect; - final Function() onImageDelete; - final String imagePath; - - @override - State createState() => _ImageGridItemState(); -} - -class _ImageGridItemState extends State { - bool showDeleteButton = false; - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) { - setState(() { - showDeleteButton = true; - }); - }, - onExit: (_) { - setState(() { - showDeleteButton = false; - }); - }, - child: Stack( - children: [ - InkWell( - onTap: widget.onImageSelect, - child: ClipRRect( - borderRadius: Corners.s8Border, - child: Image.file(File(widget.imagePath), fit: BoxFit.cover), - ), - ), - if (showDeleteButton) - Positioned( - right: 2, - top: 2, - child: FlowyIconButton( - fillColor: - Theme.of(context).colorScheme.surface.withOpacity(0.8), - hoverColor: - Theme.of(context).colorScheme.surface.withOpacity(0.8), - iconPadding: const EdgeInsets.all(5), - width: 28, - icon: FlowySvg( - FlowySvgs.delete_s, - color: Theme.of(context).colorScheme.tertiary, - ), - onPressed: widget.onImageDelete, - ), - ), - ], - ), - ); - } -} - @visibleForTesting class ColorItem extends StatelessWidget { const ColorItem({ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart deleted file mode 100644 index 7c5f0b4548..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/infra/svg.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class Svg extends StatelessWidget { - const Svg({ - super.key, - this.name, - this.width, - this.height, - this.color, - this.number, - this.padding, - }); - - final String? name; - final double? width; - final double? height; - final Color? color; - final int? number; - final EdgeInsets? padding; - - final _defaultWidth = 20.0; - final _defaultHeight = 20.0; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding ?? const EdgeInsets.all(0), - child: _buildSvg(), - ); - } - - Widget _buildSvg() { - if (name != null) { - return SvgPicture.asset( - 'assets/images/$name.svg', - colorFilter: - color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null, - fit: BoxFit.fill, - height: height, - width: width, - package: 'appflowy_editor_plugins', - ); - } else if (number != null) { - final numberText = - '$number.'; - return SvgPicture.string( - numberText, - width: width ?? _defaultWidth, - height: height ?? _defaultHeight, - ); - } - return const SizedBox.shrink(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart index 7b141f5351..f7a77a9816 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart @@ -1,271 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; - -// convert the current block to other block types -// only show in single selection and text type -final mobileAddBlockToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState, ___) { - if (!onlyShowInSingleSelectionAndTextType(editorState)) { - return null; - } - return const FlowySvg( - FlowySvgs.add_m, - size: Size.square(48), - ); - }, - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - return BlocksMenu( - items: _addBlockMenuItems, - editorState: editorState, - service: service, - ); - }, -); - -final _addBlockMenuItems = [ - // paragraph - BlockMenuItem( - blockType: ParagraphBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_text_decoration_m), - label: LocaleKeys.editor_text.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - paragraphNode(), - ); - }, - ), - - // to-do list - BlockMenuItem( - blockType: TodoListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_checkbox_m), - label: LocaleKeys.editor_checkbox.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - todoListNode(checked: false), - ); - }, - ), - - // heading 1 - 3 - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h1_m), - label: LocaleKeys.editor_heading1.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - headingNode(level: 1), - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h2_m), - label: LocaleKeys.editor_heading2.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - headingNode(level: 2), - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h3_m), - label: LocaleKeys.editor_heading3.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - headingNode(level: 3), - ); - }, - ), - - // bulleted list - BlockMenuItem( - blockType: BulletedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_bulleted_list_m), - label: LocaleKeys.editor_bulletedList.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - bulletedListNode(), - ); - }, - ), - - // numbered list - BlockMenuItem( - blockType: NumberedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_numbered_list_m), - label: LocaleKeys.editor_numberedList.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - numberedListNode(), - ); - }, - ), - - // toggle list - BlockMenuItem( - blockType: ToggleListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_toggle_list_m), - label: LocaleKeys.document_plugins_toggleList.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - toggleListBlockNode(), - ); - }, - ), - - // quote - BlockMenuItem( - blockType: QuoteBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_quote_m), - label: LocaleKeys.editor_quote.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - quoteNode(), - ); - }, - ), - - // callout - BlockMenuItem( - blockType: CalloutBlockKeys.type, - icon: const Icon(Icons.note_rounded), - label: LocaleKeys.document_plugins_callout.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - calloutNode(), - ); - }, - ), - - // code - BlockMenuItem( - blockType: CodeBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_code_m), - label: LocaleKeys.document_selectionMenu_codeBlock.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertBlockOrReplaceCurrentBlock( - selection, - codeBlockNode(), - ); - }, - ), - - // divider - BlockMenuItem( - blockType: DividerBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_divider_m), - label: LocaleKeys.editor_divider.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertDivider(selection); - }, - ), - - // math equation - BlockMenuItem( - blockType: MathEquationBlockKeys.type, - icon: const FlowySvg( - FlowySvgs.math_lg, - size: Size.square(22), - ), - label: LocaleKeys.document_plugins_mathEquation_name.tr(), - isSelected: _unSelectable, - onTap: (editorState, selection, service) async { - service.closeItemMenu(); - await editorState.insertMathEquation(selection); - }, - ), -]; - -bool _unSelectable( - EditorState editorState, - Selection selection, -) { - return false; -} extension EditorStateAddBlock on EditorState { - Future insertBlockOrReplaceCurrentBlock( - Selection selection, - Node insertedNode, - ) async { - // If the current block is not an empty paragraph block, - // then insert a new block below the current block. - final node = getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - final transaction = this.transaction; - if (node.type != ParagraphBlockKeys.type || - (node.delta?.isNotEmpty ?? true)) { - final path = node.path.next; - // insert the block below the current empty paragraph block - transaction - ..insertNode(path, insertedNode) - ..afterSelection = Selection.collapsed( - Position(path: path), - ); - } else { - final path = node.path; - // replace the current empty paragraph block with the inserted block - transaction - ..insertNode(path, insertedNode) - ..deleteNode(node) - ..afterSelection = Selection.collapsed( - Position(path: path), - ) - ..selectionExtraInfo = null; - } - await apply(transaction); - service.keyboardService?.enableKeyBoard(selection); - } - Future insertMathEquation( Selection selection, ) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart deleted file mode 100644 index 38e0407185..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -final mobileAlignToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const FlowySvg( - FlowySvgs.toolbar_align_center_s, - size: Size.square(32), - ) - : null; - }, - itemMenuBuilder: (_, editorState, ___) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - return _MobileAlignMenu( - editorState: editorState, - selection: selection, - ); - }, -); - -class _MobileAlignMenu extends StatelessWidget { - const _MobileAlignMenu({ - required this.editorState, - required this.selection, - }); - - final Selection selection; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - return GridView.count( - padding: EdgeInsets.zero, - crossAxisCount: 3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 3, - shrinkWrap: true, - children: [ - _buildAlignmentButton( - context, - 'left', - LocaleKeys.document_plugins_optionAction_left.tr(), - ), - _buildAlignmentButton( - context, - 'center', - LocaleKeys.document_plugins_optionAction_center.tr(), - ), - _buildAlignmentButton( - context, - 'right', - LocaleKeys.document_plugins_optionAction_right.tr(), - ), - ], - ); - } - - Widget _buildAlignmentButton( - BuildContext context, - String alignment, - String label, - ) { - final nodes = editorState.getNodesInSelection(selection); - if (nodes.isEmpty) { - const SizedBox.shrink(); - } - - bool isSatisfyCondition(bool Function(Object? value) test) { - return nodes.every( - (n) => test(n.attributes[blockComponentAlign]), - ); - } - - final data = switch (alignment) { - 'left' => FlowySvgs.toolbar_align_left_s, - 'center' => FlowySvgs.toolbar_align_center_s, - 'right' => FlowySvgs.toolbar_align_right_s, - _ => throw UnimplementedError(), - }; - final isSelected = isSatisfyCondition((value) => value == alignment); - - return MobileToolbarItemMenuBtn( - icon: FlowySvg(data, size: const Size.square(28)), - label: FlowyText(label), - isSelected: isSelected, - onPressed: () async { - await editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: alignment, - }, - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart deleted file mode 100644 index 7ab072533f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_toolbar_item.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart'; -import 'package:appflowy/plugins/base/color/color_picker_screen.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final mobileBlockSettingsToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInSingleSelectionAndTextType(editorState) - ? const FlowySvg(FlowySvgs.three_dots_s) - : null; - }, - actionHandler: (_, editorState) async { - // show the settings page - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - final context = node?.context; - if (node == null || context == null) { - return; - } - - await _showBlockActionSheet( - context, - editorState, - node, - selection, - ); - }, -); - -Future _showBlockActionSheet( - BuildContext context, - EditorState editorState, - Node node, - Selection selection, -) async { - final result = await showMobileBottomSheet( - context, - showDragHandle: true, - showCloseButton: true, - showHeader: true, - padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), - title: LocaleKeys.document_plugins_action.tr(), - builder: (context) { - return BlockActionBottomSheet( - extendActionWidgets: [ - Row( - children: [ - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_color_m, - text: LocaleKeys.document_plugins_optionAction_color.tr(), - onTap: () async { - final option = await context.push( - Uri( - path: MobileColorPickerScreen.routeName, - queryParameters: { - MobileColorPickerScreen.pageTitle: LocaleKeys - .document_plugins_optionAction_color - .tr(), - }, - ).toString(), - ); - if (option != null) { - final transaction = editorState.transaction; - transaction.updateNode(node, { - blockComponentBackgroundColor: option.id, - }); - await editorState.apply(transaction); - } - if (context.mounted) { - context.pop(true); - } - }, - ), - ), - // more options ... - ], - ), - ], - onAction: (action) async { - context.pop(true); - - final transaction = editorState.transaction; - switch (action) { - case BlockActionBottomSheetType.delete: - transaction.deleteNode(node); - break; - case BlockActionBottomSheetType.duplicate: - transaction.insertNode( - node.path.next, - node.copyWith(), - ); - break; - case BlockActionBottomSheetType.insertAbove: - case BlockActionBottomSheetType.insertBelow: - final path = action == BlockActionBottomSheetType.insertAbove - ? node.path - : node.path.next; - transaction - ..insertNode( - path, - paragraphNode(), - ) - ..afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - break; - default: - } - - if (transaction.operations.isNotEmpty) { - await editorState.apply(transaction); - } - }, - ); - }, - ); - - if (result != true) { - // restore the selection - editorState.selection = selection; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart deleted file mode 100644 index 2ca2098c61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class BlockMenuItem { - const BlockMenuItem({ - required this.blockType, - required this.icon, - required this.label, - required this.onTap, - this.isSelected, - }); - - // block type - final String blockType; - final Widget icon; - final String label; - // callback - final void Function( - EditorState editorState, - Selection selection, - // used to control the open or close the menu - MobileToolbarWidgetService service, - ) onTap; - - final bool Function( - EditorState editorState, - Selection selection, - )? isSelected; -} - -class BlocksMenu extends StatelessWidget { - const BlocksMenu({ - super.key, - required this.editorState, - required this.items, - required this.service, - }); - - final EditorState editorState; - final List items; - final MobileToolbarWidgetService service; - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 4, - padding: const EdgeInsets.only( - bottom: 36.0, - ), - shrinkWrap: true, - children: items.map((item) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - bool isSelected = false; - if (item.isSelected != null) { - isSelected = item.isSelected!(editorState, selection); - } else { - isSelected = _isSelected(editorState, selection, item.blockType); - } - return MobileToolbarItemMenuBtn( - icon: item.icon, - label: FlowyText(item.label), - isSelected: isSelected, - onPressed: () async { - item.onTap(editorState, selection, service); - }, - ); - }).toList(), - ); - } - - bool _isSelected( - EditorState editorState, - Selection selection, - String blockType, - ) { - final node = editorState.getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - return false; - } - return type == blockType; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart deleted file mode 100644 index 903fe3604e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; - -// convert the current block to other block types -// only show in single selection and text type -final mobileConvertBlockToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState, ___) { - if (!onlyShowInSingleSelectionAndTextType(editorState)) { - return null; - } - return const FlowySvg( - FlowySvgs.convert_s, - size: Size.square(22), - ); - }, - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - return BlocksMenu( - items: _convertToBlockMenuItems, - editorState: editorState, - service: service, - ); - }, -); - -final _convertToBlockMenuItems = [ - // paragraph - BlockMenuItem( - blockType: ParagraphBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_text_decoration_m), - label: LocaleKeys.editor_text.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - ParagraphBlockKeys.type, - selection: selection, - ), - ), - - // to-do list - BlockMenuItem( - blockType: TodoListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_checkbox_m), - label: LocaleKeys.editor_checkbox.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - TodoListBlockKeys.type, - selection: selection, - extraAttributes: { - TodoListBlockKeys.checked: false, - }, - ), - ), - - // heading 1 - 3 - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h1_m), - label: LocaleKeys.editor_heading1.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 1, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 1, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 1, - }, - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h2_m), - label: LocaleKeys.editor_heading2.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 2, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 2, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 2, - }, - ); - }, - ), - BlockMenuItem( - blockType: HeadingBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_h3_m), - label: LocaleKeys.editor_heading3.tr(), - isSelected: (editorState, selection) => _isHeadingSelected( - editorState, - selection, - 3, - ), - onTap: (editorState, selection, _) { - final isSelected = _isHeadingSelected( - editorState, - selection, - 3, - ); - editorState.convertBlockType( - HeadingBlockKeys.type, - selection: selection, - isSelected: isSelected, - extraAttributes: { - HeadingBlockKeys.level: 3, - }, - ); - }, - ), - - // bulleted list - BlockMenuItem( - blockType: BulletedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_bulleted_list_m), - label: LocaleKeys.editor_bulletedList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - BulletedListBlockKeys.type, - selection: selection, - ), - ), - - // numbered list - BlockMenuItem( - blockType: NumberedListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_numbered_list_m), - label: LocaleKeys.editor_numberedList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - NumberedListBlockKeys.type, - selection: selection, - ), - ), - - // toggle list - BlockMenuItem( - blockType: ToggleListBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_toggle_list_m), - label: LocaleKeys.document_plugins_toggleList.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - selection: selection, - ToggleListBlockKeys.type, - ), - ), - - // quote - BlockMenuItem( - blockType: QuoteBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_quote_m), - label: LocaleKeys.editor_quote.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - selection: selection, - QuoteBlockKeys.type, - ), - ), - - // callout - BlockMenuItem( - blockType: CalloutBlockKeys.type, - // FIXME: update icon - icon: const Icon(Icons.note_rounded), - label: LocaleKeys.document_plugins_callout.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - CalloutBlockKeys.type, - selection: selection, - extraAttributes: { - CalloutBlockKeys.icon: '📌', - }, - ), - ), - - // code - BlockMenuItem( - blockType: CodeBlockKeys.type, - icon: const FlowySvg(FlowySvgs.m_code_m), - label: LocaleKeys.document_selectionMenu_codeBlock.tr(), - onTap: (editorState, selection, _) => editorState.convertBlockType( - CodeBlockKeys.type, - selection: selection, - ), - ), -]; - -bool _isHeadingSelected( - EditorState editorState, - Selection selection, - int level, -) { - final node = editorState.getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - return false; - } - return type == HeadingBlockKeys.type && - node.attributes[HeadingBlockKeys.level] == level; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart deleted file mode 100644 index adc1026f91..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_item.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -final mobileIndentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const Icon(Icons.format_indent_increase_rounded) - : null; - }, - actionHandler: (_, editorState) { - indentCommand.execute(editorState); - }, -); - -final mobileOutdentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState, __) { - return onlyShowInTextType(editorState) - ? const Icon(Icons.format_indent_decrease_rounded) - : null; - }, - actionHandler: (_, editorState) { - outdentCommand.execute(editorState); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart deleted file mode 100644 index dddda0758d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.text_s, - size: Size.square(24), - ), - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - return _TextDecorationMenu( - editorState, - selection, - service, - ); - }, -); - -class _TextDecorationMenu extends StatefulWidget { - const _TextDecorationMenu( - this.editorState, - this.selection, - this.service, - ); - - final EditorState editorState; - final Selection selection; - final MobileToolbarWidgetService service; - - @override - State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); -} - -class _TextDecorationMenuState extends State<_TextDecorationMenu> { - EditorState get editorState => widget.editorState; - - final textDecorations = [ - // BIUS - TextDecorationUnit( - icon: AFMobileIcons.bold, - label: AppFlowyEditorL10n.current.bold, - name: AppFlowyRichTextKeys.bold, - ), - TextDecorationUnit( - icon: AFMobileIcons.italic, - label: AppFlowyEditorL10n.current.italic, - name: AppFlowyRichTextKeys.italic, - ), - TextDecorationUnit( - icon: AFMobileIcons.underline, - label: AppFlowyEditorL10n.current.underline, - name: AppFlowyRichTextKeys.underline, - ), - TextDecorationUnit( - icon: AFMobileIcons.strikethrough, - label: AppFlowyEditorL10n.current.strikethrough, - name: AppFlowyRichTextKeys.strikethrough, - ), - - // Code - TextDecorationUnit( - icon: AFMobileIcons.code, - label: AppFlowyEditorL10n.current.embedCode, - name: AppFlowyRichTextKeys.code, - ), - - // link - TextDecorationUnit( - icon: AFMobileIcons.link, - label: AppFlowyEditorL10n.current.link, - name: AppFlowyRichTextKeys.href, - ), - ]; - - @override - void dispose() { - widget.editorState.selectionExtraInfo = null; - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final children = textDecorations - .map((currentDecoration) { - // Check current decoration is active or not - final selection = widget.selection; - - // only show edit link bottom sheet when selection is not collapsed - if (selection.isCollapsed && - currentDecoration.name == AppFlowyRichTextKeys.href) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final bool isSelected; - if (selection.isCollapsed) { - isSelected = editorState.toggledStyle.containsKey( - currentDecoration.name, - ); - } else { - isSelected = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[currentDecoration.name] == true, - ); - }); - } - - return MobileToolbarItemMenuBtn( - icon: AFMobileIcon( - afMobileIcons: currentDecoration.icon, - color: MobileToolbarTheme.of(context).iconColor, - ), - label: FlowyText(currentDecoration.label), - isSelected: isSelected, - onPressed: () { - if (currentDecoration.name == AppFlowyRichTextKeys.href) { - if (selection.isCollapsed) { - return; - } - - _closeKeyboard(); - - // show edit link bottom sheet - final context = nodes.firstOrNull?.context; - if (context != null) { - final text = editorState - .getTextInSelection( - widget.selection, - ) - .join(); - final href = - editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.href, - widget.selection, - ); - showEditLinkBottomSheet( - context, - text, - href, - (context, newText, newHref) { - _updateTextAndHref(text, href, newText, newHref); - context.pop(); - }, - ); - } - } else { - setState(() { - editorState.toggleAttribute(currentDecoration.name); - }); - } - }, - ); - }) - .nonNulls - .toList(); - - return GridView.count( - shrinkWrap: true, - padding: EdgeInsets.zero, - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 4, - children: children, - ); - } - - void _closeKeyboard() { - editorState.updateSelectionWithReason( - widget.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ); - editorState.service.keyboardService?.closeKeyboard(); - } - - void _updateTextAndHref( - String prevText, - String? prevHref, - String text, - String href, - ) async { - final selection = widget.selection; - if (!selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - final transaction = editorState.transaction; - if (prevText != text) { - transaction.replaceText( - node, - selection.startIndex, - selection.length, - text, - ); - } - // if the text is empty, it means the user wants to remove the text - if (text.isNotEmpty && prevHref != href) { - transaction.formatText(node, selection.startIndex, text.length, { - AppFlowyRichTextKeys.href: href.isEmpty ? null : href, - }); - } - await editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart deleted file mode 100644 index dddda0758d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item_v2.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.text_s, - size: Size.square(24), - ), - itemMenuBuilder: (_, editorState, service) { - final selection = editorState.selection; - if (selection == null) { - return const SizedBox.shrink(); - } - return _TextDecorationMenu( - editorState, - selection, - service, - ); - }, -); - -class _TextDecorationMenu extends StatefulWidget { - const _TextDecorationMenu( - this.editorState, - this.selection, - this.service, - ); - - final EditorState editorState; - final Selection selection; - final MobileToolbarWidgetService service; - - @override - State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); -} - -class _TextDecorationMenuState extends State<_TextDecorationMenu> { - EditorState get editorState => widget.editorState; - - final textDecorations = [ - // BIUS - TextDecorationUnit( - icon: AFMobileIcons.bold, - label: AppFlowyEditorL10n.current.bold, - name: AppFlowyRichTextKeys.bold, - ), - TextDecorationUnit( - icon: AFMobileIcons.italic, - label: AppFlowyEditorL10n.current.italic, - name: AppFlowyRichTextKeys.italic, - ), - TextDecorationUnit( - icon: AFMobileIcons.underline, - label: AppFlowyEditorL10n.current.underline, - name: AppFlowyRichTextKeys.underline, - ), - TextDecorationUnit( - icon: AFMobileIcons.strikethrough, - label: AppFlowyEditorL10n.current.strikethrough, - name: AppFlowyRichTextKeys.strikethrough, - ), - - // Code - TextDecorationUnit( - icon: AFMobileIcons.code, - label: AppFlowyEditorL10n.current.embedCode, - name: AppFlowyRichTextKeys.code, - ), - - // link - TextDecorationUnit( - icon: AFMobileIcons.link, - label: AppFlowyEditorL10n.current.link, - name: AppFlowyRichTextKeys.href, - ), - ]; - - @override - void dispose() { - widget.editorState.selectionExtraInfo = null; - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final children = textDecorations - .map((currentDecoration) { - // Check current decoration is active or not - final selection = widget.selection; - - // only show edit link bottom sheet when selection is not collapsed - if (selection.isCollapsed && - currentDecoration.name == AppFlowyRichTextKeys.href) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final bool isSelected; - if (selection.isCollapsed) { - isSelected = editorState.toggledStyle.containsKey( - currentDecoration.name, - ); - } else { - isSelected = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[currentDecoration.name] == true, - ); - }); - } - - return MobileToolbarItemMenuBtn( - icon: AFMobileIcon( - afMobileIcons: currentDecoration.icon, - color: MobileToolbarTheme.of(context).iconColor, - ), - label: FlowyText(currentDecoration.label), - isSelected: isSelected, - onPressed: () { - if (currentDecoration.name == AppFlowyRichTextKeys.href) { - if (selection.isCollapsed) { - return; - } - - _closeKeyboard(); - - // show edit link bottom sheet - final context = nodes.firstOrNull?.context; - if (context != null) { - final text = editorState - .getTextInSelection( - widget.selection, - ) - .join(); - final href = - editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.href, - widget.selection, - ); - showEditLinkBottomSheet( - context, - text, - href, - (context, newText, newHref) { - _updateTextAndHref(text, href, newText, newHref); - context.pop(); - }, - ); - } - } else { - setState(() { - editorState.toggleAttribute(currentDecoration.name); - }); - } - }, - ); - }) - .nonNulls - .toList(); - - return GridView.count( - shrinkWrap: true, - padding: EdgeInsets.zero, - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 4, - children: children, - ); - } - - void _closeKeyboard() { - editorState.updateSelectionWithReason( - widget.selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ); - editorState.service.keyboardService?.closeKeyboard(); - } - - void _updateTextAndHref( - String prevText, - String? prevHref, - String text, - String href, - ) async { - final selection = widget.selection; - if (!selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - final transaction = editorState.transaction; - if (prevText != text) { - transaction.replaceText( - node, - selection.startIndex, - selection.length, - text, - ); - } - // if the text is empty, it means the user wants to remove the text - if (text.isNotEmpty && prevHref != href) { - transaction.formatText(node, selection.startIndex, text.length, { - AppFlowyRichTextKeys.href: href.isEmpty ? null : href, - }); - } - await editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart deleted file mode 100644 index 7fc162868d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/redo_mobile_toolbar_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final redoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.m_redo_m, - ), - actionHandler: (_, editorState) async { - editorState.undoManager.redo(); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart deleted file mode 100644 index 2b22ac1ada..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/undo_redo/undo_mobile_toolbar_item.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final undoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __, ___) => const FlowySvg( - FlowySvgs.m_undo_m, - ), - actionHandler: (_, editorState) async { - editorState.undoManager.undo(); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart deleted file mode 100644 index c8609c114e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -class ExportPageWidget extends StatelessWidget { - const ExportPageWidget({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const FlowyText.medium( - 'Open document failed', - fontSize: 18.0, - ), - const VSpace(5), - const FlowyText.regular( - 'Please try to export the page and contact us.', - fontSize: 12.0, - ), - const VSpace(20), - RoundedTextButton( - title: 'Export page', - width: 100, - height: 30, - onPressed: onTap, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/trash/menu.dart b/frontend/appflowy_flutter/lib/plugins/trash/menu.dart deleted file mode 100644 index 414d61d659..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/trash/menu.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -class MenuTrash extends StatelessWidget { - const MenuTrash({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greySelect, - ), - isSelected: () => getIt().latestOpenView == null, - child: SizedBox( - height: 26, - child: InkWell( - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - child: _render(context), - ), - ).padding(horizontal: Insets.l), - ).padding(horizontal: 8); - }, - ); - } - - Widget _render(BuildContext context) { - return Row( - children: [ - const FlowySvg( - FlowySvgs.trash_m, - size: Size(16, 16), - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.trash_text.tr()), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart deleted file mode 100644 index 5a7bac2c75..0000000000 --- a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:http/http.dart' as http; - -Future isImageExistOnCloud({ - required String url, - required UserProfilePB userProfilePB, -}) async { - final header = {}; - final token = userProfilePB.token; - try { - final decodedToken = jsonDecode(token); - header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; - final response = await http.get(Uri.http(url), headers: header); - return response.statusCode == 200; - } catch (_) { - return false; - } -} diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 99e4b9f092..bd735aa4a7 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -79,15 +79,10 @@ void _resolveCommonService( IntegrationMode mode, ) async { getIt.registerFactory(() => FilePicker()); - if (mode.isTest) { - getIt.registerFactory( - () => MockApplicationDataStorage(), - ); - } else { - getIt.registerFactory( - () => ApplicationDataStorage(), - ); - } + + getIt.registerFactory( + () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(), + ); getIt.registerFactoryAsync( () async { diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 9de85f1c6d..ece9cef8a8 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; + import 'package:appflowy/core/notification/folder_notification.dart'; import 'package:appflowy/core/notification/user_notification.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -18,7 +20,6 @@ typedef DidUserWorkspaceUpdateCallback = void Function( RepeatedUserWorkspacePB workspaces, ); typedef UserProfileNotifyValue = FlowyResult; -typedef AuthNotifyValue = FlowyResult; class UserListener { UserListener({ diff --git a/frontend/appflowy_flutter/lib/util/base64_string.dart b/frontend/appflowy_flutter/lib/util/base64_string.dart deleted file mode 100644 index 01a7f9a17e..0000000000 --- a/frontend/appflowy_flutter/lib/util/base64_string.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'dart:convert'; - -extension Base64Encode on String { - String get base64 => base64Encode(utf8.encode(this)); -} diff --git a/frontend/appflowy_flutter/lib/util/built_in_svgs.dart b/frontend/appflowy_flutter/lib/util/built_in_svgs.dart new file mode 100644 index 0000000000..6e7f2087b4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/built_in_svgs.dart @@ -0,0 +1,13 @@ +final builtInSVGIcons = [ + '1F9CC', + '1F9DB', + '1F9DD-200D-2642-FE0F', + '1F9DE-200D-2642-FE0F', + '1F9DF', + '1F42F', + '1F43A', + '1F431', + '1F435', + '1F600', + '1F984', +]; diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 98e8bcf08e..2df1c95c1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -4,7 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' show WorkspaceSettingPB; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flowy_infra/time/duration.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'home_bloc.freezed.dart'; @@ -65,22 +64,6 @@ class HomeBloc extends Bloc { } } -enum MenuResizeType { - slide, - drag, -} - -extension MenuResizeTypeExtension on MenuResizeType { - Duration duration() { - switch (this) { - case MenuResizeType.drag: - return 30.milliseconds; - case MenuResizeType.slide: - return 350.milliseconds; - } - } -} - @freezed class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart index 58071fb68e..249144096c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart @@ -39,9 +39,6 @@ class MenuUserBloc extends Bloc { _userListener.start(onProfileUpdated: _profileUpdated); await _initUser(); }, - fetchWorkspaces: () async { - // - }, didReceiveUserProfile: (UserProfilePB newUserProfile) { emit(state.copyWith(userProfile: newUserProfile)); }, @@ -70,9 +67,7 @@ class MenuUserBloc extends Bloc { return; } userProfileOrFailed.fold( - (newUserProfile) => add( - MenuUserEvent.didReceiveUserProfile(newUserProfile), - ), + (profile) => add(MenuUserEvent.didReceiveUserProfile(profile)), (err) => Log.error(err), ); } @@ -81,7 +76,6 @@ class MenuUserBloc extends Bloc { @freezed class MenuUserEvent with _$MenuUserEvent { const factory MenuUserEvent.initial() = _Initial; - const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces; const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName; const factory MenuUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 6996432fc7..8925fc9bda 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @override @@ -119,6 +120,7 @@ class DesktopAppearance extends BaseAppearance { tint8: theme.tint8, tint9: theme.tint9, textColor: theme.text, + secondaryTextColor: theme.secondaryText, greyHover: theme.hoverBG1, greySelect: theme.bg3, lightGreyHover: theme.hoverBG3, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 9eb424d4b0..19cd87f4f4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; + // ThemeData in mobile import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { static const _primaryColor = Color(0xFF00BCF0); //primary 100 @@ -28,9 +29,7 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, ); - final codeFontStyle = getFontStyle( - fontFamily: codeFontFamily, - ); + final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); final theme = brightness == Brightness.light ? appTheme.lightTheme @@ -81,9 +80,7 @@ class MobileAppearance extends BaseAppearance { : _hintColorInDarkMode; return ThemeData( - // color useMaterial3: false, - primaryColor: colorTheme.primary, //primary 100 primaryColorLight: const Color(0xFF57B5F8), //primary 80 dividerColor: colorTheme.outline, //caption @@ -124,6 +121,7 @@ class MobileAppearance extends BaseAppearance { ), ), shadowColor: MaterialStateProperty.all(null), + foregroundColor: MaterialStateProperty.all(Colors.white), backgroundColor: MaterialStateProperty.resolveWith( (Set states) { if (states.contains(MaterialState.disabled)) { @@ -132,7 +130,6 @@ class MobileAppearance extends BaseAppearance { return colorTheme.primary; }, ), - foregroundColor: MaterialStateProperty.all(Colors.white), ), ), outlinedButtonTheme: OutlinedButtonThemeData( @@ -144,20 +141,13 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w500, ), ), - foregroundColor: MaterialStateProperty.all( - colorTheme.onBackground, - ), + foregroundColor: MaterialStateProperty.all(colorTheme.onBackground), backgroundColor: MaterialStateProperty.all(colorTheme.background), shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), side: MaterialStateProperty.all( - BorderSide( - color: colorTheme.outline, - width: 0.5, - ), + BorderSide(color: colorTheme.outline, width: 0.5), ), padding: MaterialStateProperty.all( const EdgeInsets.symmetric(horizontal: 8, vertical: 12), @@ -166,9 +156,7 @@ class MobileAppearance extends BaseAppearance { ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( - textStyle: MaterialStateProperty.all( - fontStyle, - ), + textStyle: MaterialStateProperty.all(fontStyle), ), ), // text @@ -262,6 +250,7 @@ class MobileAppearance extends BaseAppearance { tint8: theme.tint8, tint9: theme.tint9, textColor: theme.text, + secondaryTextColor: theme.secondaryText, greyHover: theme.hoverBG1, greySelect: theme.bg3, lightGreyHover: theme.hoverBG3, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 2d73d6ebe7..906ccb78f2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -9,10 +9,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_dialog_bloc.freezed.dart'; enum SettingsPage { + // NEW + account, + // OLD appearance, language, files, - user, + // user, notifications, cloud, shortcuts, @@ -88,6 +91,6 @@ class SettingsDialogState with _$SettingsDialogState { SettingsDialogState( userProfile: userProfile, successOrFailure: FlowyResult.success(null), - page: SettingsPage.appearance, + page: SettingsPage.account, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 0f45b019fd..59127bdab0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -111,14 +111,12 @@ class SettingsUserViewBloc extends Bloc { void _profileUpdated( FlowyResult userProfileOrFailed, - ) { - userProfileOrFailed.fold( - (newUserProfile) { - add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)); - }, - (err) => Log.error(err), - ); - } + ) => + userProfileOrFailed.fold( + (newUserProfile) => + add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart index 00f88e153d..8762833353 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -1,7 +1,9 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + class FolderHeader extends StatefulWidget { const FolderHeader({ super.key, @@ -40,6 +42,7 @@ class _FolderHeaderState extends State { constraints: const BoxConstraints( minHeight: iconSize + textPadding * 2, ), + fontColor: AFThemeExtension.of(context).textColor, padding: const EdgeInsets.all(textPadding), fillColor: Colors.transparent, onPressed: widget.onPressed, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart index b64af1967f..45698ebabf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart @@ -62,14 +62,10 @@ class UserSettingButton extends StatelessWidget { } } -void showSettingsDialog( - BuildContext context, - UserProfilePB userProfile, -) { - showDialog( - context: context, - builder: (dialogContext) { - return BlocProvider.value( +void showSettingsDialog(BuildContext context, UserProfilePB userProfile) => + showDialog( + context: context, + builder: (dialogContext) => BlocProvider.value( key: _settingsDialogKey, value: BlocProvider.of(dialogContext), child: SettingsDialog( @@ -81,10 +77,9 @@ void showSettingsDialog( }, dismissDialog: () { if (Navigator.of(dialogContext).canPop()) { - Navigator.of(dialogContext).pop(); - } else { - Log.warn("Can't pop dialog context"); + return Navigator.of(dialogContext).pop(); } + Log.warn("Can't pop dialog context"); }, restartApp: () async { // Pop the dialog using the dialog context @@ -92,7 +87,5 @@ void showSettingsDialog( await runAppFlowy(); }, ), - ); - }, - ); -} + ), + ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index 288bd76a74..dc089e27a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; @@ -8,7 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // keep this widget in case we need to roll back (lucas.xu) @@ -23,10 +24,8 @@ class SidebarUser extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => MenuUserBloc(userProfile) - ..add( - const MenuUserEvent.initial(), - ), + create: (_) => + MenuUserBloc(userProfile)..add(const MenuUserEvent.initial()), child: BlocBuilder( builder: (context, state) => Row( children: [ @@ -35,9 +34,7 @@ class SidebarUser extends StatelessWidget { name: state.userProfile.name, ), const HSpace(8), - Expanded( - child: _buildUserName(context, state), - ), + Expanded(child: _buildUserName(context, state)), UserSettingButton(userProfile: state.userProfile), const HSpace(4), const NotificationButton(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index dddc78e87a..8a81377c1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -15,8 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; -typedef NaviAction = void Function(); - class NavigationNotifier with ChangeNotifier { NavigationNotifier({required this.navigationItems}); @@ -145,19 +143,6 @@ class NaviItemWidget extends StatelessWidget { } } -class NaviItemDivider extends StatelessWidget { - const NaviItemDivider({super.key, required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Row( - children: [child, const Text('/')], - ); - } -} - class EllipsisNaviItem extends NavigationItem { EllipsisNaviItem({required this.items}); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart new file mode 100644 index 0000000000..aa3c7f8702 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -0,0 +1,473 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsAccountView extends StatefulWidget { + const SettingsAccountView({ + super.key, + required this.userProfile, + required this.didLogin, + required this.didLogout, + }); + + final UserProfilePB userProfile; + + // Called when the user signs in from the setting dialog + final VoidCallback didLogin; + + // Called when the user logout in the setting dialog + final VoidCallback didLogout; + + @override + State createState() => _SettingsAccountViewState(); +} + +class _SettingsAccountViewState extends State { + late String userName = widget.userProfile.name; + late final TextEditingController _emailController = TextEditingController( + text: widget.userProfile.email, + ); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + getIt(param1: widget.userProfile) + ..add(const SettingsUserEvent.initial()), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.userProfile.email != current.userProfile.email, + listener: (context, state) => + _emailController.text = state.userProfile.email, + builder: (context, state) { + return SettingsBody( + children: [ + SettingsHeader( + title: LocaleKeys.settings_accountPage_title.tr(), + description: LocaleKeys.settings_accountPage_description.tr(), + ), + SettingsCategory( + title: LocaleKeys.settings_accountPage_general_title.tr(), + children: [ + UserProfileSetting( + name: userName, + iconUrl: state.userProfile.iconUrl, + onSave: (newName) { + // Pseudo change the name to update the UI before the backend + // processes the request. This is to give the user a sense of + // immediate feedback, and avoid UI flickering. + setState(() => userName = newName); + context + .read() + .add(SettingsUserEvent.updateUserName(newName)); + }, + ), + ], + ), + // Only show change email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_accountPage_email_title.tr(), + children: [ + SingleSettingAction( + label: state.userProfile.email, + buttonLabel: LocaleKeys + .settings_accountPage_email_actions_change + .tr(), + onPressed: () => SettingsAlertDialog( + title: LocaleKeys + .settings_accountPage_email_actions_change + .tr(), + confirmLabel: LocaleKeys.button_save.tr(), + confirm: () { + context.read().add( + SettingsUserEvent.updateUserEmail( + _emailController.text, + ), + ); + Navigator.of(context).pop(); + }, + children: [ + SettingsInputField( + label: LocaleKeys.settings_accountPage_email_title + .tr(), + value: state.userProfile.email, + hideActions: true, + textController: _emailController, + ), + ], + ).show(context), + ), + ], + ), + ], + + /// Enable when we have change password feature and 2FA + // const SettingsCategorySpacer(), + // SettingsCategory( + // title: 'Account & security', + // children: [ + // SingleSettingAction( + // label: '**********', + // buttonLabel: 'Change password', + // onPressed: () {}, + // ), + // SingleSettingAction( + // label: '2-step authentication', + // buttonLabel: 'Enable 2FA', + // onPressed: () {}, + // ), + // ], + // ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_accountPage_keys_title.tr(), + children: [ + SettingsInputField( + label: + LocaleKeys.settings_accountPage_keys_openAILabel.tr(), + tooltip: + LocaleKeys.settings_accountPage_keys_openAITooltip.tr(), + placeholder: + LocaleKeys.settings_accountPage_keys_openAIHint.tr(), + value: state.userProfile.openaiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(key)), + ), + SettingsInputField( + label: LocaleKeys.settings_accountPage_keys_stabilityAILabel + .tr(), + tooltip: LocaleKeys + .settings_accountPage_keys_stabilityAITooltip + .tr(), + placeholder: LocaleKeys + .settings_accountPage_keys_stabilityAIHint + .tr(), + value: state.userProfile.stabilityAiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserStabilityAIKey(key)), + ), + ], + ), + const SettingsCategorySpacer(), + SettingsCategory( + title: LocaleKeys.settings_accountPage_login_title.tr(), + children: [ + SignInOutButton( + userProfile: state.userProfile, + onAction: + state.userProfile.authenticator == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, + ), + ], + ), + + /// Enable when we can delete accounts + // const SettingsCategorySpacer(), + // SettingsSubcategory( + // title: 'Delete account', + // children: [ + // SingleSettingAction( + // label: + // 'Permanently delete your account and remove access from all teamspaces.', + // labelMaxLines: 4, + // onPressed: () {}, + // buttonLabel: 'Delete my account', + // isDangerous: true, + // fontSize: 12, + // ), + // ], + // ), + ], + ); + }, + ), + ); + } +} + +@visibleForTesting +class SignInOutButton extends StatelessWidget { + const SignInOutButton({ + super.key, + required this.userProfile, + required this.onAction, + this.signIn = true, + }); + + final UserProfilePB userProfile; + final VoidCallback onAction; + final bool signIn; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 48, + child: FlowyTextButton( + signIn + ? LocaleKeys.settings_accountPage_login_loginLabel.tr() + : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () => SettingsAlertDialog( + title: LocaleKeys.settings_accountPage_login_loginLabel.tr(), + subtitle: signIn + ? null + : switch (userProfile.encryptionType) { + EncryptionTypePB.Symmetric => LocaleKeys + .settings_menu_selfEncryptionLogoutPrompt + .tr(), + _ => LocaleKeys.settings_menu_logoutPrompt.tr(), + }, + implyLeading: signIn, + confirm: !signIn + ? () async { + await getIt().signOut(); + onAction(); + } + : null, + children: + signIn ? [SettingThirdPartyLogin(didLogin: onAction)] : null, + ).show(context), + ), + ), + ], + ); + } +} + +@visibleForTesting +class UserProfileSetting extends StatefulWidget { + const UserProfileSetting({ + super.key, + required this.name, + required this.iconUrl, + this.onSave, + }); + + final String name; + final String iconUrl; + final void Function(String)? onSave; + + @override + State createState() => _UserProfileSettingState(); +} + +class _UserProfileSettingState extends State { + late final FocusNode focusNode; + bool isEditing = false; + bool isHovering = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + ); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showIconPickerDialog(context), + child: FlowyHover( + resetHoverOnRebuild: false, + onHover: (state) => setState(() => isHovering = state), + style: HoverStyle( + hoverColor: Colors.transparent, + borderRadius: BorderRadius.circular(100), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + UserAvatar( + iconUrl: widget.iconUrl, + name: widget.name, + isLarge: true, + isHovering: isHovering, + ), + const VSpace(4), + FlowyText.regular( + LocaleKeys + .settings_accountPage_general_changeProfilePicture + .tr(), + color: AFThemeExtension.of(context).textColor, + ), + ], + ), + ), + ), + if (widget.iconUrl.isNotEmpty) + Positioned( + right: 0, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const SettingsUserEvent.removeUserIcon()), + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: FlowyHover( + resetHoverOnRebuild: false, + style: const HoverStyle( + borderRadius: BorderRadius.all(Radius.circular(24)), + hoverColor: Color(0xFF005483), + ), + builder: (_, __) => Padding( + padding: const EdgeInsets.all(4), + child: FlowySvg( + FlowySvgs.close_s, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), + ), + ), + ], + ), + const HSpace(16), + if (!isEditing) ...[ + Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.medium( + widget.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ], + ), + ), + ] else ...[ + Flexible( + child: SettingsInputField( + value: widget.name, + focusNode: focusNode..requestFocus(), + onCancel: () => setState(() => isEditing = false), + onSave: (val) { + widget.onSave?.call(val); + setState(() => isEditing = false); + }, + ), + ), + ], + ], + ); + } + + Future _showIconPickerDialog(BuildContext context) { + return showDialog( + context: context, + builder: (dialogContext) => SimpleDialog( + title: FlowyText.medium( + LocaleKeys.settings_user_selectAnIcon.tr(), + fontSize: FontSizes.s16, + ), + children: [ + Container( + height: 380, + width: 360, + margin: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl: emoji)); + Navigator.of(dialogContext).pop(); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 2647591757..a3b34d524f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,6 +1,8 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; @@ -9,18 +11,12 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_s import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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'; import 'widgets/setting_cloud.dart'; -const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12); -const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0); - class SettingsDialog extends StatelessWidget { SettingsDialog( this.user, { @@ -41,49 +37,31 @@ class SettingsDialog extends StatelessWidget { ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => FlowyDialog( - title: Padding( - padding: _dialogHorizontalPadding + _contentInsetPadding, - child: FlowyText( - LocaleKeys.settings_title.tr(), - fontSize: 20, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.tertiary, - ), - ), width: MediaQuery.of(context).size.width * 0.7, child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, - body: Padding( - padding: _dialogHorizontalPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - userProfile: user, - changeSelectedPage: (index) { - context - .read() - .add(SettingsDialogEvent.setSelectedPage(index)); - }, - currentPage: - context.read().state.page, - ), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + userProfile: user, + changeSelectedPage: (index) => context + .read() + .add(SettingsDialogEvent.setSelectedPage(index)), + currentPage: + context.read().state.page, ), - VerticalDivider( - color: Theme.of(context).dividerColor, + ), + Expanded( + child: getSettingsView( + context.read().state.page, + context.read().state.userProfile, ), - const SizedBox(width: 10), - Expanded( - child: getSettingsView( - context.read().state.page, - context.read().state.userProfile, - ), - ), - ], - ), + ), + ], ), ), ), @@ -94,27 +72,24 @@ class SettingsDialog extends StatelessWidget { Widget getSettingsView(SettingsPage page, UserProfilePB user) { switch (page) { + case SettingsPage.account: + return SettingsAccountView( + userProfile: user, + didLogout: didLogout, + didLogin: dismissDialog, + ); case SettingsPage.appearance: return const SettingsAppearanceView(); case SettingsPage.language: return const SettingsLanguageView(); case SettingsPage.files: return const SettingsFileSystemView(); - case SettingsPage.user: - return SettingsUserView( - user, - didLogin: () => dismissDialog(), - didLogout: didLogout, - didOpenUser: restartApp, - ); case SettingsPage.notifications: return const SettingsNotificationsView(); case SettingsPage.cloud: - return SettingCloud( - restartAppFlowy: () => restartApp(), - ); + return SettingCloud(restartAppFlowy: () => restartApp()); case SettingsPage.shortcuts: - return const SettingsCustomizeShortcutsWrapper(); + return const SettingsShortcutsView(); case SettingsPage.member: return WorkspaceMembersPage(userProfile: user); case SettingsPage.featureFlags: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart new file mode 100644 index 0000000000..17d60359cd --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; + +class SettingsAlertDialog extends StatefulWidget { + const SettingsAlertDialog({ + super.key, + required this.title, + this.subtitle, + this.children, + this.cancel, + this.confirm, + this.confirmLabel, + this.hideCancelButton = false, + this.isDangerous = false, + this.implyLeading = false, + }); + + final String title; + final String? subtitle; + final List? children; + final void Function()? cancel; + final void Function()? confirm; + final String? confirmLabel; + final bool hideCancelButton; + final bool isDangerous; + + /// If true, a back button will show in the top left corner + final bool implyLeading; + + @override + State createState() => _SettingsAlertDialogState(); +} + +class _SettingsAlertDialogState extends State { + @override + Widget build(BuildContext context) { + return StyledDialog( + maxHeight: 600, + maxWidth: 600, + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.implyLeading) ...[ + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + children: [ + const FlowySvg( + FlowySvgs.arrow_back_m, + size: Size.square(24), + ), + const HSpace(8), + FlowyText.semibold( + LocaleKeys.button_back.tr(), + fontSize: 16, + ), + ], + ), + ), + ), + ], + const Spacer(), + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowySvg( + FlowySvgs.m_close_m, + size: const Size.square(20), + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: FlowyText.medium( + widget.title, + fontSize: 22, + color: Theme.of(context).colorScheme.tertiary, + maxLines: null, + ), + ), + ], + ), + if (widget.subtitle?.isNotEmpty ?? false) ...[ + const VSpace(16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: FlowyText.regular( + widget.subtitle!, + fontSize: 16, + color: Theme.of(context).colorScheme.tertiary, + textAlign: TextAlign.center, + maxLines: null, + ), + ), + ], + ), + ], + if (widget.children?.isNotEmpty ?? false) ...[ + const VSpace(16), + ...widget.children!, + ], + if (widget.confirm != null || !widget.hideCancelButton) ...[ + const VSpace(20), + ], + _Actions( + hideCancelButton: widget.hideCancelButton, + confirmLabel: widget.confirmLabel, + cancel: widget.cancel, + confirm: widget.confirm, + isDangerous: widget.isDangerous, + ), + ], + ), + ); + } +} + +class _Actions extends StatelessWidget { + const _Actions({ + required this.hideCancelButton, + this.confirmLabel, + this.cancel, + this.confirm, + this.isDangerous = false, + }); + + final bool hideCancelButton; + final String? confirmLabel; + final VoidCallback? cancel; + final VoidCallback? confirm; + final bool isDangerous; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!hideCancelButton) ...[ + SizedBox( + height: 24, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontColor: AFThemeExtension.of(context).textColor, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + onPressed: () { + cancel?.call(); + Navigator.of(context).pop(); + }, + ), + ), + ], + if (confirm != null && !hideCancelButton) ...[ + const HSpace(8), + ], + if (confirm != null) ...[ + SizedBox( + height: 48, + child: FlowyTextButton( + confirmLabel ?? LocaleKeys.button_confirm.tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontColor: isDangerous ? Colors.white : null, + fontHoverColor: Colors.white, + fillColor: isDangerous + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + hoverColor: isDangerous + ? Theme.of(context).colorScheme.error + : const Color(0xFF005483), + onPressed: confirm, + ), + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart new file mode 100644 index 0000000000..b1ade9c118 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class SettingsBody extends StatelessWidget { + const SettingsBody({ + super.key, + required this.children, + }); + + final List children; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart new file mode 100644 index 0000000000..34f25dd41e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; + +/// Renders a simple category taking a title and the list +/// of children (settings) to be rendered. +/// +class SettingsCategory extends StatelessWidget { + const SettingsCategory({ + super.key, + required this.title, + this.description, + this.tooltip, + this.actions, + required this.children, + }); + + final String title; + final String? description; + final String? tooltip; + final List? actions; + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FlowyText.semibold( + title, + maxLines: 2, + fontSize: 16, + overflow: TextOverflow.ellipsis, + ), + if (tooltip != null) ...[ + const HSpace(4), + FlowyTooltip( + message: tooltip, + child: const FlowySvg(FlowySvgs.information_s), + ), + ], + const Spacer(), + if (actions != null) ...actions!, + ], + ), + const VSpace(8), + if (description?.isNotEmpty ?? false) ...[ + FlowyText.regular( + description!, + maxLines: 4, + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + const VSpace(8), + ], + SeparatedColumn( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + separatorBuilder: () => + children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart new file mode 100644 index 0000000000..5637fdd20c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +/// This is used to create a uniform space and divider +/// between categories in settings. +/// +class SettingsCategorySpacer extends StatelessWidget { + const SettingsCategorySpacer({super.key}); + + @override + Widget build(BuildContext context) => + const Divider(height: 32, color: Color(0xFFF2F2F2)); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart new file mode 100644 index 0000000000..c028e6886d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +/// Renders a simple header for the settings view +/// +class SettingsHeader extends StatelessWidget { + const SettingsHeader({super.key, required this.title, this.description}); + + final String title; + final String? description; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold(title, fontSize: 24), + if (description?.isNotEmpty == true) ...[ + const VSpace(8), + FlowyText( + description!, + maxLines: 4, + fontSize: 12, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ], + const VSpace(16), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart new file mode 100644 index 0000000000..de0526b717 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; + +/// This is used to describe a settings input field +/// +/// The input will have secondary action of "save" and "cancel" +/// which will only be shown when the input has changed. +/// +/// _Note: The label can overflow and will be ellipsized._ +/// +class SettingsInputField extends StatefulWidget { + const SettingsInputField({ + super.key, + this.label, + this.textController, + this.focusNode, + this.obscureText = false, + this.value, + this.placeholder, + this.tooltip, + this.onSave, + this.onCancel, + this.hideActions = false, + }); + + final String? label; + final TextEditingController? textController; + final FocusNode? focusNode; + + /// If true, the input field will be obscured + /// and an option to toggle to show the text will be provided. + /// + final bool obscureText; + + final String? value; + final String? placeholder; + final String? tooltip; + + /// If true the save and cancel options will not show below the + /// input field. + /// + final bool hideActions; + + final Function(String)? onSave; + + /// The action to be performed when the cancel button is pressed. + /// + /// If null the button will **NOT** be disabled! Instead it will + /// reset the input to the original value. + /// + final Function()? onCancel; + + @override + State createState() => _SettingsInputFieldState(); +} + +class _SettingsInputFieldState extends State { + late final controller = + widget.textController ?? TextEditingController(text: widget.value); + late final FocusNode focusNode = widget.focusNode ?? FocusNode(); + late bool obscureText = widget.obscureText; + + @override + void dispose() { + if (widget.focusNode == null) { + focusNode.dispose(); + } + if (widget.textController == null) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + if (widget.label?.isNotEmpty == true) ...[ + Flexible( + child: FlowyText.medium( + widget.label!, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ), + ], + if (widget.tooltip != null) ...[ + const HSpace(4), + FlowyTooltip( + message: widget.tooltip, + child: const FlowySvg(FlowySvgs.information_s), + ), + ], + ], + ), + const VSpace(8), + SizedBox( + height: 48, + child: FlowyTextField( + focusNode: focusNode, + hintText: widget.placeholder, + controller: controller, + autoFocus: false, + obscureText: obscureText, + isDense: false, + suffixIconConstraints: + BoxConstraints.tight(const Size(23 + 18, 24)), + suffixIcon: !widget.obscureText + ? null + : GestureDetector( + onTap: () => setState(() => obscureText = !obscureText), + child: Padding( + padding: const EdgeInsets.only(right: 18), + child: FlowySvg( + obscureText ? FlowySvgs.show_m : FlowySvgs.hide_m, + size: const Size(12, 15), + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + onSubmitted: widget.onSave, + onChanged: (_) => setState(() {}), + ), + ), + if (!widget.hideActions && + ((widget.value == null && controller.text.isNotEmpty) || + widget.value != null && widget.value != controller.text)) ...[ + const VSpace(8), + Row( + children: [ + const Spacer(), + SizedBox( + height: 21, + child: FlowyTextButton( + LocaleKeys.button_save.tr(), + fontWeight: FontWeight.normal, + padding: EdgeInsets.zero, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + onPressed: () => widget.onSave?.call(controller.text), + ), + ), + const HSpace(24), + SizedBox( + height: 21, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + fontWeight: FontWeight.normal, + padding: EdgeInsets.zero, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: AFThemeExtension.of(context).textColor, + onPressed: () { + setState(() => controller.text = widget.value ?? ''); + widget.onCancel?.call(); + }, + ), + ), + ], + ), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart new file mode 100644 index 0000000000..4be6dbee05 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +/// Renders a simple category taking a title and the list +/// of children (settings) to be rendered. +/// +class SettingsSubcategory extends StatelessWidget { + const SettingsSubcategory({ + super.key, + required this.title, + required this.children, + }); + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + title, + color: AFThemeExtension.of(context).secondaryTextColor, + maxLines: 2, + fontSize: 14, + overflow: TextOverflow.ellipsis, + ), + const VSpace(8), + SeparatedColumn( + mainAxisSize: MainAxisSize.min, + separatorBuilder: () => + children.length > 1 ? const VSpace(16) : const SizedBox.shrink(), + children: children, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart new file mode 100644 index 0000000000..7d51ea370c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +/// This is used to describe a single setting action +/// +/// This will render a simple action that takes the title, +/// the button label, and the button action. +/// +/// _Note: The label can overflow and will be ellipsized, +/// unless maxLines is overriden._ +/// +class SingleSettingAction extends StatelessWidget { + const SingleSettingAction({ + super.key, + required this.label, + this.labelMaxLines, + required this.buttonLabel, + this.onPressed, + this.isDangerous = false, + this.fontSize = 14, + this.fontWeight = FontWeight.normal, + }); + + final String label; + final int? labelMaxLines; + final String buttonLabel; + + /// The action to be performed when the button is pressed + /// + /// If null the button will be rendered as disabled. + /// + final VoidCallback? onPressed; + + /// If isDangerous is true, the button will be rendered as a dangerous + /// action, with a red outline. + /// + final bool isDangerous; + + final double fontSize; + final FontWeight fontWeight; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: FlowyText( + label, + fontSize: fontSize, + fontWeight: fontWeight, + maxLines: labelMaxLines, + overflow: TextOverflow.ellipsis, + color: AFThemeExtension.of(context).secondaryTextColor, + ), + ), + const HSpace(24), + SizedBox( + height: 32, + child: FlowyTextButton( + buttonLabel, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + fillColor: + isDangerous ? null : Theme.of(context).colorScheme.primary, + hoverColor: isDangerous ? null : const Color(0xFF005483), + fontColor: isDangerous ? Theme.of(context).colorScheme.error : null, + fontHoverColor: Colors.white, + fontSize: 12, + isDangerous: isDangerous, + onPressed: onPressed, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart index 9865372105..a1fb8257ec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -1,7 +1,11 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class FeatureFlagsPage extends StatelessWidget { const FeatureFlagsPage({ @@ -10,36 +14,30 @@ class FeatureFlagsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: SeparatedColumn( - children: [ - ...FeatureFlag.data.entries + return SettingsBody( + children: [ + const SettingsHeader(title: 'Feature flags'), + SeparatedColumn( + children: FeatureFlag.data.entries .where((e) => e.key != FeatureFlag.unknown) - .map( - (e) => _FeatureFlagItem(featureFlag: e.key), - ), - FlowyTextButton( - 'Restart the app to apply changes', - fontSize: 16.0, - fontColor: Colors.red, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - onPressed: () async { - await runAppFlowy(); - }, - ), - ], - ), + .map((e) => _FeatureFlagItem(featureFlag: e.key)) + .toList(), + ), + const SettingsCategorySpacer(), + FlowyTextButton( + 'Restart the app to apply changes', + fontSize: 16.0, + fontColor: Colors.red, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + onPressed: () async => runAppFlowy(), + ), + ], ); } } class _FeatureFlagItem extends StatefulWidget { - const _FeatureFlagItem({ - required this.featureFlag, - }); + const _FeatureFlagItem({required this.featureFlag}); final FeatureFlag featureFlag; @@ -51,21 +49,11 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { @override Widget build(BuildContext context) { return ListTile( - title: FlowyText( - widget.featureFlag.name, - fontSize: 16.0, - ), - subtitle: FlowyText.small( - widget.featureFlag.description, - maxLines: 3, - ), + title: FlowyText(widget.featureFlag.name, fontSize: 16.0), + subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3), trailing: Switch.adaptive( value: widget.featureFlag.isOn, - onChanged: (value) { - setState(() { - widget.featureFlag.update(value); - }); - }, + onChanged: (value) => setState(() => widget.featureFlag.update(value)), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart index 5fdc8ff7cc..e7cc9fff0d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; @@ -9,8 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -19,9 +20,7 @@ import '../../../../../startup/startup.dart'; import '../../../../../startup/tasks/prelude.dart'; class SettingsFileLocationCustomizer extends StatefulWidget { - const SettingsFileLocationCustomizer({ - super.key, - }); + const SettingsFileLocationCustomizer({super.key}); @override State createState() => diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index 9779ed7632..f44cd67e67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,7 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -13,15 +18,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; class WorkspaceMembersPage extends StatelessWidget { - const WorkspaceMembersPage({ - super.key, - required this.userProfile, - }); + const WorkspaceMembersPage({super.key, required this.userProfile}); final UserProfilePB userProfile; @@ -33,25 +34,22 @@ class WorkspaceMembersPage extends StatelessWidget { child: BlocConsumer( listener: _showResultDialog, builder: (context, state) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // title - FlowyText.semibold( - LocaleKeys.settings_appearance_members_title.tr(), - fontSize: 20, + return SettingsBody( + children: [ + // title + SettingsHeader( + title: LocaleKeys.settings_appearance_members_title.tr(), + ), + if (state.myRole.canInvite) const _InviteMember(), + if (state.myRole.canInvite && state.members.isNotEmpty) + const SettingsCategorySpacer(), + if (state.members.isNotEmpty) + _MemberList( + members: state.members, + userProfile: userProfile, + myRole: state.myRole, ), - if (state.myRole.canInvite) const _InviteMember(), - if (state.members.isNotEmpty) - _MemberList( - members: state.members, - userProfile: userProfile, - myRole: state.myRole, - ), - const VSpace(48.0), - ], - ), + ], ); }, ), @@ -117,7 +115,6 @@ class _InviteMemberState extends State<_InviteMember> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const VSpace(12.0), FlowyText.semibold( LocaleKeys.settings_appearance_members_inviteMembers.tr(), fontSize: 16.0, @@ -151,7 +148,6 @@ class _InviteMemberState extends State<_InviteMember> { ), ], ), - const VSpace(16.0), /* Enable this when the feature is ready PrimaryButton( backgroundColor: const Color(0xFFE0E0E0), @@ -183,10 +179,6 @@ class _InviteMemberState extends State<_InviteMember> { ), const VSpace(16.0), */ - const Divider( - height: 1.0, - thickness: 1.0, - ), ], ); } @@ -194,11 +186,10 @@ class _InviteMemberState extends State<_InviteMember> { void _inviteMember() { final email = _emailController.text; if (!isEmail(email)) { - showSnackBarMessage( + return showSnackBarMessage( context, LocaleKeys.settings_appearance_members_emailInvalidError.tr(), ); - return; } context .read() @@ -219,22 +210,17 @@ class _MemberList extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( + return SeparatedColumn( + crossAxisAlignment: CrossAxisAlignment.start, + separatorBuilder: () => const Divider(), children: [ - const VSpace(16.0), - SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const Divider(), - children: [ - const _MemberListHeader(), - ...members.map( - (member) => _MemberItem( - member: member, - myRole: myRole, - userProfile: userProfile, - ), - ), - ], + const _MemberListHeader(), + ...members.map( + (member) => _MemberItem( + member: member, + myRole: myRole, + userProfile: userProfile, + ), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 8ba18e1d92..8d6b48976e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -6,6 +8,8 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -13,7 +17,6 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.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:go_router/go_router.dart'; @@ -37,8 +40,11 @@ class SettingCloud extends StatelessWidget { create: (context) => CloudSettingBloc(cloudType), child: BlocBuilder( builder: (context, state) { - return Column( + return SettingsBody( children: [ + SettingsHeader( + title: LocaleKeys.settings_menu_cloudSettings.tr(), + ), if (Env.enableCustomCloud) Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index e9ccc32b0e..e623652f8b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; @@ -10,7 +12,6 @@ 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:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingThirdPartyLogin extends StatelessWidget { @@ -42,24 +43,12 @@ class SettingThirdPartyLogin extends StatelessWidget { : const SizedBox.shrink(); return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.signIn_signInWith.tr(), - fontSize: 16, - ), - const HSpace(6), - ], - ), - const VSpace(6), promptMessage, const VSpace(6), indicator, const VSpace(6), if (isAuthEnabled) const ThirdPartySignInButtons(), - const VSpace(6), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart index 73ed265c1d..c41e5704f0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -1,10 +1,15 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'settings_appearance/settings_appearance.dart'; @@ -14,59 +19,57 @@ class SettingsAppearanceView extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: BlocProvider( - create: (_) => DynamicPluginBloc(), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - ColorSchemeSetting( - currentTheme: state.appTheme.themeName, - bloc: context.read(), - ), - BrightnessSetting( - currentThemeMode: state.themeMode, - ), - const Divider(), - ThemeFontFamilySetting( - currentFontFamily: state.font, - ), - const Divider(), - DocumentCursorColorSetting( - currentCursorColor: state.documentCursorColor ?? - DefaultAppearanceSettings.getDefaultDocumentCursorColor( - context, - ), - ), - DocumentSelectionColorSetting( - currentSelectionColor: state.documentSelectionColor ?? - DefaultAppearanceSettings - .getDefaultDocumentSelectionColor( - context, - ), - ), - const Divider(), - LayoutDirectionSetting( - currentLayoutDirection: state.layoutDirection, - ), - TextDirectionSetting( - currentTextDirection: state.textDirection, - ), - const EnableRTLToolbarItemsSetting(), - const Divider(), - DateFormatSetting( - currentFormat: state.dateFormat, - ), - TimeFormatSetting( - currentFormat: state.timeFormat, - ), - const Divider(), - CreateFileSettings(), - ], - ); - }, - ), + return BlocProvider( + create: (_) => DynamicPluginBloc(), + child: BlocBuilder( + builder: (context, state) { + return SettingsBody( + children: [ + SettingsHeader(title: LocaleKeys.settings_menu_appearance.tr()), + ColorSchemeSetting( + currentTheme: state.appTheme.themeName, + bloc: context.read(), + ), + BrightnessSetting( + currentThemeMode: state.themeMode, + ), + const Divider(), + ThemeFontFamilySetting( + currentFontFamily: state.font, + ), + const Divider(), + DocumentCursorColorSetting( + currentCursorColor: state.documentCursorColor ?? + DefaultAppearanceSettings.getDefaultDocumentCursorColor( + context, + ), + ), + DocumentSelectionColorSetting( + currentSelectionColor: state.documentSelectionColor ?? + DefaultAppearanceSettings.getDefaultDocumentSelectionColor( + context, + ), + ), + const Divider(), + LayoutDirectionSetting( + currentLayoutDirection: state.layoutDirection, + ), + TextDirectionSetting( + currentTextDirection: state.textDirection, + ), + const EnableRTLToolbarItemsSetting(), + const Divider(), + DateFormatSetting( + currentFormat: state.dateFormat, + ), + TimeFormatSetting( + currentFormat: state.timeFormat, + ), + const Divider(), + CreateFileSettings(), + ], + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index b302e6466d..169c53e802 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -1,55 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SettingsCustomizeShortcutsWrapper extends StatelessWidget { - const SettingsCustomizeShortcutsWrapper({super.key}); +class SettingsShortcutsView extends StatelessWidget { + const SettingsShortcutsView({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(), - child: const SettingsCustomizeShortcutsView(), - ); - } -} - -class SettingsCustomizeShortcutsView extends StatelessWidget { - const SettingsCustomizeShortcutsView({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - switch (state.status) { - case ShortcutsStatus.initial: - case ShortcutsStatus.updating: - return const Center(child: CircularProgressIndicator()); - case ShortcutsStatus.success: - return ShortcutsListView(shortcuts: state.commandShortcutEvents); - case ShortcutsStatus.failure: - return ShortcutsErrorView( - errorMessage: state.error, - ); - } - }, + child: SettingsBody( + children: [ + SettingsHeader( + title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), + ), + BlocBuilder( + builder: (_, state) => switch (state.status) { + ShortcutsStatus.initial || + ShortcutsStatus.updating => + const Center(child: CircularProgressIndicator()), + ShortcutsStatus.success => + ShortcutsListView(shortcuts: state.commandShortcutEvents), + ShortcutsStatus.failure => + ShortcutsErrorView(errorMessage: state.error), + }, + ), + ], + ), ); } } class ShortcutsListView extends StatelessWidget { - const ShortcutsListView({ - super.key, - required this.shortcuts, - }); + const ShortcutsListView({super.key, required this.shortcuts}); final List shortcuts; @@ -73,14 +67,7 @@ class ShortcutsListView extends StatelessWidget { ], ), const VSpace(10), - Expanded( - child: ListView.builder( - itemCount: shortcuts.length, - itemBuilder: (context, index) => ShortcutsListTile( - shortcutEvent: shortcuts[index], - ), - ), - ), + ...shortcuts.map((e) => ShortcutsListTile(shortcutEvent: e)), const VSpace(10), Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -88,9 +75,7 @@ class ShortcutsListView extends StatelessWidget { const Spacer(), FlowyTextButton( LocaleKeys.settings_shortcuts_resetToDefault.tr(), - onPressed: () { - context.read().resetToDefault(); - }, + onPressed: () => context.read().resetToDefault(), ), ], ), @@ -248,9 +233,7 @@ class ShortcutsErrorView extends StatelessWidget { ), FlowyIconButton( icon: const Icon(Icons.replay_outlined), - onPressed: () { - BlocProvider.of(context).fetchShortcuts(); - }, + onPressed: () => context.read().fetchShortcuts(), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart index 1def9a7b2b..047cfa62cb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart @@ -1,35 +1,33 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; -class SettingsFileSystemView extends StatefulWidget { - const SettingsFileSystemView({ - super.key, - }); - - @override - State createState() => _SettingsFileSystemViewState(); -} - -class _SettingsFileSystemViewState extends State { - late final _items = [ - const SettingsFileLocationCustomizer(), - // disable export data for v0.2.0 in release mode. - if (kDebugMode) const SettingsExportFileWidget(), - const ImportAppFlowyData(), - // clear the cache - const SettingsFileCacheWidget(), - ]; +class SettingsFileSystemView extends StatelessWidget { + const SettingsFileSystemView({super.key}); @override Widget build(BuildContext context) { - return SeparatedColumn( - separatorBuilder: () => const Divider(), - children: _items, + return SettingsBody( + children: [ + SettingsHeader(title: LocaleKeys.settings_menu_files.tr()), + const SettingsFileLocationCustomizer(), + const SettingsCategorySpacer(), + if (kDebugMode) ...[ + const SettingsExportFileWidget(), + ], + const ImportAppFlowyData(), + const SettingsCategorySpacer(), + const SettingsFileCacheWidget(), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart index 45243b27c8..32ab4db5f3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -1,11 +1,14 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flowy_infra/language.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsLanguageView extends StatelessWidget { @@ -13,18 +16,21 @@ class SettingsLanguageView extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) => Row( - children: [ - Expanded( - child: FlowyText.medium( - LocaleKeys.settings_menu_language.tr(), + return BlocBuilder( + builder: (context, state) => SettingsBody( + children: [ + SettingsHeader(title: LocaleKeys.settings_menu_language.tr()), + Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.settings_menu_language.tr(), + ), ), - ), - LanguageSelector(currentLocale: state.locale), - ], - ), + LanguageSelector(currentLocale: state.locale), + ], + ), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index fd39b126c8..8dfa1217b2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,12 +1,15 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; class SettingsMenu extends StatelessWidget { const SettingsMenu({ @@ -22,79 +25,127 @@ class SettingsMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: SeparatedColumn( - separatorBuilder: () => const SizedBox(height: 10), - children: [ - SettingsMenuElement( - page: SettingsPage.appearance, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_appearance.tr(), - icon: Icons.brightness_4, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.language, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_language.tr(), - icon: Icons.translate, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.files, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_files.tr(), - icon: Icons.file_present_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.user, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_user.tr(), - icon: Icons.account_box_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.notifications, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_notifications.tr(), - icon: Icons.notifications_outlined, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.cloud, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: Icons.sync, - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.shortcuts, - selectedPage: currentPage, - label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), - icon: Icons.cut, - changeSelectedPage: changeSelectedPage, - ), - if (FeatureFlag.membersSettings.isOn && - userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) - SettingsMenuElement( - page: SettingsPage.member, - selectedPage: currentPage, - label: LocaleKeys.settings_appearance_members_label.tr(), - icon: Icons.people, - changeSelectedPage: changeSelectedPage, + // Column > Expanded for full size no matter the content + return Column( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 8, right: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), ), - if (kDebugMode) - SettingsMenuElement( - // no need to translate this page - page: SettingsPage.featureFlags, - selectedPage: currentPage, - label: 'Feature Flags', - icon: Icons.flag, - changeSelectedPage: changeSelectedPage, + child: SingleChildScrollView( + // Right padding is added to make the scrollbar centered + // in the space between the menu and the content + padding: const EdgeInsets.only(right: 4) + + const EdgeInsets.symmetric(vertical: 16), + physics: const ClampingScrollPhysics(), + child: SeparatedColumn( + separatorBuilder: () => const VSpace(16), + children: [ + SettingsMenuElement( + page: SettingsPage.account, + selectedPage: currentPage, + label: LocaleKeys.settings_accountPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_account_m), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.appearance, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_appearance.tr(), + icon: Icon( + Icons.brightness_4, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.language, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_language.tr(), + icon: Icon( + Icons.translate, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.files, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_files.tr(), + icon: Icon( + Icons.file_present_outlined, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.notifications, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_notifications.tr(), + icon: Icon( + Icons.notifications_outlined, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.cloud, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_cloudSettings.tr(), + icon: Icon( + Icons.sync, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.shortcuts, + selectedPage: currentPage, + label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), + icon: Icon( + Icons.cut, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + if (FeatureFlag.membersSettings.isOn && + userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) + SettingsMenuElement( + page: SettingsPage.member, + selectedPage: currentPage, + label: LocaleKeys.settings_appearance_members_label.tr(), + icon: Icon( + Icons.people, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + if (kDebugMode) + SettingsMenuElement( + // no need to translate this page + page: SettingsPage.featureFlags, + selectedPage: currentPage, + label: 'Feature Flags', + icon: Icon( + Icons.flag, + color: AFThemeExtension.of(context).textColor, + ), + changeSelectedPage: changeSelectedPage, + ), + ], + ), ), - ], - ), + ), + ), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart index ad02cd9df6..d06b08d4db 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart @@ -1,8 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; class SettingsMenuElement extends StatelessWidget { const SettingsMenuElement({ @@ -17,27 +19,22 @@ class SettingsMenuElement extends StatelessWidget { final SettingsPage page; final SettingsPage selectedPage; final String label; - final IconData icon; + final Widget icon; final Function changeSelectedPage; @override Widget build(BuildContext context) { return FlowyHover( + isSelected: () => page == selectedPage, resetHoverOnRebuild: false, style: HoverStyle( - hoverColor: Theme.of(context).colorScheme.primary, + hoverColor: AFThemeExtension.of(context).greySelect, + borderRadius: BorderRadius.circular(4), ), child: ListTile( - leading: Icon( - icon, - size: 16, - color: page == selectedPage - ? Theme.of(context).colorScheme.onSurface - : null, - ), - onTap: () { - changeSelectedPage(page); - }, + dense: true, + leading: icon, + onTap: () => changeSelectedPage(page), selected: page == selectedPage, selectedColor: Theme.of(context).colorScheme.onSurface, selectedTileColor: Theme.of(context).colorScheme.primary, @@ -45,7 +42,7 @@ class SettingsMenuElement extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), minLeadingWidth: 0, - title: FlowyText.semibold( + title: FlowyText.medium( label, fontSize: FontSizes.s14, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart index 0986cac82a..a930b55edf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart @@ -1,8 +1,11 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsNotificationsView extends StatelessWidget { @@ -12,32 +15,26 @@ class SettingsNotificationsView extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowySettingListTile( - label: LocaleKeys - .settings_notifications_enableNotifications_label - .tr(), - hint: LocaleKeys.settings_notifications_enableNotifications_hint - .tr(), - trailing: [ - Switch( - value: state.isNotificationsEnabled, - splashRadius: 0, - activeColor: Theme.of(context).colorScheme.primary, - onChanged: (value) { - context - .read() - .toggleNotificationsEnabled(); - }, - ), - ], - ), - ], - ), + return SettingsBody( + children: [ + SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()), + FlowySettingListTile( + label: LocaleKeys.settings_notifications_enableNotifications_label + .tr(), + hint: LocaleKeys.settings_notifications_enableNotifications_hint + .tr(), + trailing: [ + Switch( + value: state.isNotificationsEnabled, + splashRadius: 0, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (value) => context + .read() + .toggleNotificationsEnabled(), + ), + ], + ), + ], ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart deleted file mode 100644 index 7ef8ccc1df..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ /dev/null @@ -1,559 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/util/debounce.dart'; -import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'setting_third_party_login.dart'; - -const defaultUserAvatar = '1F600'; -const _iconSize = Size(60, 60); - -class SettingsUserView extends StatelessWidget { - SettingsUserView( - this.user, { - required this.didLogin, - required this.didLogout, - required this.didOpenUser, - }) : super(key: ValueKey(user.id)); - - // Called when the user login in the setting dialog - final VoidCallback didLogin; - // Called when the user logout in the setting dialog - final VoidCallback didLogout; - // Called when the user open a historical user in the setting dialog - final VoidCallback didOpenUser; - final UserProfilePB user; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(param1: user) - ..add(const SettingsUserEvent.initial()), - child: BlocBuilder( - builder: (context, state) => SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildUserIconSetting(context), - if (isAuthEnabled && - user.authenticator != AuthenticatorPB.Local) ...[ - const VSpace(12), - UserEmailInput(user.email), - ], - const VSpace(12), - _renderCurrentOpenaiKey(context), - const VSpace(12), - _renderCurrentStabilityAIKey(context), - const VSpace(12), - _renderLoginOrLogoutButton(context, state), - const VSpace(12), - ], - ), - ), - ), - ); - } - - Row _buildUserIconSetting(BuildContext context) { - return Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _showIconPickerDialog(context), - child: FlowyHover( - style: const HoverStyle.transparent(), - builder: (context, onHover) { - Widget avatar = UserAvatar( - iconUrl: user.iconUrl, - name: user.name, - isLarge: true, - ); - - if (onHover) { - avatar = _avatarOverlay( - context: context, - hasIcon: user.iconUrl.isNotEmpty, - child: avatar, - ); - } - - return avatar; - }, - ), - ), - const HSpace(12), - Flexible(child: _renderUserNameInput(context)), - ], - ); - } - - Future _showIconPickerDialog(BuildContext context) { - return showDialog( - context: context, - builder: (dialogContext) => SimpleDialog( - title: FlowyText.medium( - LocaleKeys.settings_user_selectAnIcon.tr(), - fontSize: FontSizes.s16, - ), - children: [ - Container( - height: 380, - width: 360, - margin: const EdgeInsets.symmetric(horizontal: 12), - child: FlowyEmojiPicker( - onEmojiSelected: (_, emoji) { - context - .read() - .add(SettingsUserEvent.updateUserIcon(iconUrl: emoji)); - Navigator.of(dialogContext).pop(); - }, - ), - ), - ], - ), - ); - } - - /// Renders either a login or logout button based on the user's authentication status, or nothing if Supabase is not enabled. - /// - /// This function checks the current user's authentication type and Supabase - /// configuration to determine whether to render a third-party login button - /// or a logout button. - Widget _renderLoginOrLogoutButton( - BuildContext context, - SettingsUserState state, - ) { - if (!isAuthEnabled) { - return const SizedBox.shrink(); - } - - // If the user is logged in locally, render a third-party login button. - if (state.userProfile.authenticator == AuthenticatorPB.Local) { - return SettingThirdPartyLogin(didLogin: didLogin); - } - - return SettingLogoutButton(user: user, didLogout: didLogout); - } - - Widget _renderUserNameInput(BuildContext context) { - final String name = - context.read().state.userProfile.name; - return UserNameInput(name); - } - - Widget _renderCurrentOpenaiKey(BuildContext context) { - final String accessKey = - context.read().state.userProfile.openaiKey; - return _AIAccessKeyInput( - accessKey: accessKey, - title: 'OpenAI Key', - hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(), - callback: (key) => context - .read() - .add(SettingsUserEvent.updateUserOpenAIKey(key)), - ); - } - - Widget _renderCurrentStabilityAIKey(BuildContext context) { - final String accessKey = - context.read().state.userProfile.stabilityAiKey; - return _AIAccessKeyInput( - accessKey: accessKey, - title: 'Stability AI Key', - hintText: LocaleKeys.settings_user_pleaseInputYourStabilityAIKey.tr(), - callback: (key) => context - .read() - .add(SettingsUserEvent.updateUserStabilityAIKey(key)), - ); - } - - Widget _avatarOverlay({ - required BuildContext context, - required bool hasIcon, - required Widget child, - }) => - FlowyTooltip( - message: LocaleKeys.settings_user_tooltipSelectIcon.tr(), - child: Stack( - children: [ - Container( - width: 56, - height: 56, - foregroundDecoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary - .withOpacity(hasIcon ? 0.8 : 0.5), - shape: BoxShape.circle, - ), - child: child, - ), - const Positioned( - top: 0, - left: 0, - bottom: 0, - right: 0, - child: Center( - child: SizedBox( - width: 32, - height: 32, - child: FlowySvg(FlowySvgs.emoji_s), - ), - ), - ), - ], - ), - ); -} - -@visibleForTesting -class UserNameInput extends StatefulWidget { - const UserNameInput(this.name, {super.key}); - - final String name; - - @override - UserNameInputState createState() => UserNameInputState(); -} - -class UserNameInputState extends State { - late TextEditingController _controller; - - Timer? _debounce; - final Duration _debounceDuration = const Duration(milliseconds: 500); - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.name); - } - - @override - void dispose() { - _controller.dispose(); - _debounce?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, - decoration: InputDecoration( - labelText: LocaleKeys.settings_user_name.tr(), - labelStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.w500), - enabledBorder: UnderlineInputBorder( - borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), - ), - ), - onChanged: (val) { - if (_debounce?.isActive ?? false) { - _debounce!.cancel(); - } - - _debounce = Timer(_debounceDuration, () { - context - .read() - .add(SettingsUserEvent.updateUserName(val)); - }); - }, - ); - } -} - -@visibleForTesting -class UserEmailInput extends StatefulWidget { - const UserEmailInput(this.email, {super.key}); - - final String email; - - @override - UserEmailInputState createState() => UserEmailInputState(); -} - -class UserEmailInputState extends State { - late TextEditingController _controller; - - Timer? _debounce; - final Duration _debounceDuration = const Duration(milliseconds: 500); - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.email); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, - decoration: InputDecoration( - labelText: LocaleKeys.settings_user_email.tr(), - labelStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.w500), - enabledBorder: UnderlineInputBorder( - borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), - ), - ), - onChanged: (val) { - if (_debounce?.isActive ?? false) { - _debounce!.cancel(); - } - - _debounce = Timer(_debounceDuration, () { - context - .read() - .add(SettingsUserEvent.updateUserEmail(val)); - }); - }, - ); - } - - @override - void dispose() { - _controller.dispose(); - _debounce?.cancel(); - super.dispose(); - } -} - -class _AIAccessKeyInput extends StatefulWidget { - const _AIAccessKeyInput({ - required this.accessKey, - required this.title, - required this.hintText, - required this.callback, - }); - - final String accessKey; - final String title; - final String hintText; - final void Function(String key) callback; - - @override - State<_AIAccessKeyInput> createState() => _AIAccessKeyInputState(); -} - -class _AIAccessKeyInputState extends State<_AIAccessKeyInput> { - bool visible = false; - final textEditingController = TextEditingController(); - final debounce = Debounce(); - - @override - void initState() { - super.initState(); - textEditingController.text = widget.accessKey; - } - - @override - void dispose() { - textEditingController.dispose(); - debounce.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: textEditingController, - obscureText: !visible, - decoration: InputDecoration( - enabledBorder: UnderlineInputBorder( - borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), - ), - labelText: widget.title, - labelStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.w500), - hintText: widget.hintText, - suffixIcon: FlowyIconButton( - width: 40, - height: 40, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - icon: Icon( - visible ? Icons.visibility : Icons.visibility_off, - ), - onPressed: () { - setState(() { - visible = !visible; - }); - }, - ), - ), - onChanged: (value) { - debounce.call(() { - widget.callback(value); - }); - }, - ); - } -} - -typedef SelectIconCallback = void Function(String iconUrl, bool isSelected); - -final builtInSVGIcons = [ - '1F9CC', - '1F9DB', - '1F9DD-200D-2642-FE0F', - '1F9DE-200D-2642-FE0F', - '1F9DF', - '1F42F', - '1F43A', - '1F431', - '1F435', - '1F600', - '1F984', -]; - -// REMOVE this widget in next version 0.3.10 -class IconGallery extends StatelessWidget { - const IconGallery({ - super.key, - required this.selectedIcon, - required this.onSelectIcon, - this.defaultOption, - }); - - final String selectedIcon; - final SelectIconCallback onSelectIcon; - final Widget? defaultOption; - - @override - Widget build(BuildContext context) { - return GridView.count( - padding: const EdgeInsets.all(20), - crossAxisCount: 5, - mainAxisSpacing: 4, - crossAxisSpacing: 4, - children: [ - if (defaultOption != null) defaultOption!, - ...builtInSVGIcons.mapIndexed( - (int index, String iconUrl) => IconOption( - emoji: FlowySvgData('emoji/$iconUrl'), - iconUrl: iconUrl, - onSelectIcon: onSelectIcon, - isSelected: iconUrl == selectedIcon, - ), - ), - ], - ); - } -} - -class IconOption extends StatelessWidget { - IconOption({ - required this.emoji, - required this.iconUrl, - required this.onSelectIcon, - required this.isSelected, - }) : super(key: ValueKey(emoji)); - - final FlowySvgData emoji; - final String iconUrl; - final SelectIconCallback onSelectIcon; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return InkWell( - borderRadius: Corners.s8Border, - hoverColor: Theme.of(context).colorScheme.tertiaryContainer, - onTap: () => onSelectIcon(iconUrl, isSelected), - child: DecoratedBox( - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - borderRadius: Corners.s8Border, - ), - child: FlowySvg( - emoji, - size: _iconSize, - blendMode: null, - ), - ), - ); - } -} - -class SettingLogoutButton extends StatelessWidget { - const SettingLogoutButton({ - super.key, - required this.user, - required this.didLogout, - }); - - final UserProfilePB user; - final VoidCallback didLogout; - - @override - Widget build(BuildContext context) { - return Center( - child: SizedBox( - width: 160, - child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 2.0), - text: FlowyText.medium( - LocaleKeys.settings_menu_logout.tr(), - fontSize: 13, - textAlign: TextAlign.center, - ), - onTap: () { - NavigatorAlertDialog( - title: logoutPromptMessage(), - confirm: () async { - await getIt().signOut(); - didLogout(); - }, - ).show(context); - }, - ), - ), - ); - } - - String logoutPromptMessage() { - switch (user.encryptionType) { - case EncryptionTypePB.Symmetric: - return LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(); - default: - return LocaleKeys.settings_menu_logoutPrompt.tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 8750eddb0b..529807c4f4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -11,7 +13,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MoreViewActions extends StatefulWidget { @@ -107,7 +108,7 @@ class _MoreViewActionsState extends State { FlowySvgs.three_dots_vertical_s, size: const Size.square(16), color: isHovering - ? Theme.of(context).colorScheme.onPrimary + ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).iconTheme.color, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 3f86b57181..4b6708c151 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,15 +1,16 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; +import 'package:appflowy/util/built_in_svgs.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; const double _smallSize = 28; -const double _largeSize = 56; +const double _largeSize = 64; class UserAvatar extends StatelessWidget { const UserAvatar({ @@ -17,12 +18,16 @@ class UserAvatar extends StatelessWidget { required this.iconUrl, required this.name, this.isLarge = false, + this.isHovering = false, }); final String iconUrl; final String name; final bool isLarge; + // If true, a border will be applied on top of the avatar + final bool isHovering; + @override Widget build(BuildContext context) { final size = isLarge ? _largeSize : _smallSize; @@ -47,6 +52,12 @@ class UserAvatar extends StatelessWidget { decoration: BoxDecoration( color: color, shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, ), child: FlowyText.semibold( nameInitials, @@ -64,16 +75,27 @@ class UserAvatar extends StatelessWidget { return SizedBox.square( dimension: size, - child: ClipRRect( - borderRadius: Corners.s5Border, - child: CircleAvatar( - backgroundColor: Colors.transparent, - child: builtInSVGIcons.contains(iconUrl) - ? FlowySvg( - FlowySvgData('emoji/$iconUrl'), - blendMode: null, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, ) - : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: builtInSVGIcons.contains(iconUrl) + ? FlowySvg( + FlowySvgData('emoji/$iconUrl'), + blendMode: null, + ) + : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + ), ), ), ); @@ -81,6 +103,15 @@ class UserAvatar extends StatelessWidget { /// Return the user name, if the user name is empty, /// return the default user name. + /// String _userName(String name) => name.isEmpty ? LocaleKeys.defaultUsername.tr() : name; + + /// Used to darken the generated color for the hover border effect. + /// The color is darkened by 15% - Hence the 0.15 value. + /// + Color _darken(Color color) { + final hsl = HSLColor.fromColor(color); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 582109a5d3..c940f68451 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -1,10 +1,11 @@ -import 'package:flowy_infra/utils/color_converter.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra/utils/color_converter.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'default_colorscheme.dart'; + import 'dandelion.dart'; +import 'default_colorscheme.dart'; import 'lavender.dart'; import 'lemonade.dart'; @@ -14,7 +15,6 @@ part 'colorscheme.g.dart'; /// /// The key is the theme name, and the value is a list of two color schemes: /// the first is for light mode, and the second is for dark mode. - const Map> themeMap = { BuiltInTheme.defaultTheme: [ DefaultColorScheme.light(), @@ -34,65 +34,8 @@ const Map> themeMap = { ], }; -@JsonSerializable( - converters: [ - ColorConverter(), - ], -) +@JsonSerializable(converters: [ColorConverter()]) class FlowyColorScheme { - final Color surface; - final Color hover; - final Color selector; - final Color red; - final Color yellow; - final Color green; - final Color shader1; - final Color shader2; - final Color shader3; - final Color shader4; - final Color shader5; - final Color shader6; - final Color shader7; - final Color bg1; - final Color bg2; - final Color bg3; - final Color bg4; - final Color tint1; - final Color tint2; - final Color tint3; - final Color tint4; - final Color tint5; - final Color tint6; - final Color tint7; - final Color tint8; - final Color tint9; - final Color main1; - final Color main2; - final Color shadow; - final Color sidebarBg; - final Color divider; - final Color topbarBg; - final Color icon; - final Color text; - final Color input; - final Color hint; - final Color primary; - final Color onPrimary; - //page title hover effect - final Color hoverBG1; - //action item hover effect - final Color hoverBG2; - final Color hoverBG3; - //the text color when it is hovered - final Color hoverFG; - final Color questionBubbleBG; - final Color progressBarBGColor; - //editor toolbar BG color - final Color toolbarColor; - final Color toggleButtonBGColor; - final Color calendarWeekendBGColor; - //grid bottom count color - final Color gridRowCountColor; const FlowyColorScheme({ required this.surface, required this.hover, @@ -128,6 +71,7 @@ class FlowyColorScheme { required this.topbarBg, required this.icon, required this.text, + required this.secondaryText, required this.input, required this.hint, required this.primary, @@ -144,6 +88,61 @@ class FlowyColorScheme { required this.gridRowCountColor, }); + final Color surface; + final Color hover; + final Color selector; + final Color red; + final Color yellow; + final Color green; + final Color shader1; + final Color shader2; + final Color shader3; + final Color shader4; + final Color shader5; + final Color shader6; + final Color shader7; + final Color bg1; + final Color bg2; + final Color bg3; + final Color bg4; + final Color tint1; + final Color tint2; + final Color tint3; + final Color tint4; + final Color tint5; + final Color tint6; + final Color tint7; + final Color tint8; + final Color tint9; + final Color main1; + final Color main2; + final Color shadow; + final Color sidebarBg; + final Color divider; + final Color topbarBg; + final Color icon; + final Color text; + final Color secondaryText; + final Color input; + final Color hint; + final Color primary; + final Color onPrimary; + //page title hover effect + final Color hoverBG1; + //action item hover effect + final Color hoverBG2; + final Color hoverBG3; + //the text color when it is hovered + final Color hoverFG; + final Color questionBubbleBG; + final Color progressBarBGColor; + //editor toolbar BG color + final Color toolbarColor; + final Color toggleButtonBGColor; + final Color calendarWeekendBGColor; + //grid bottom count color + final Color gridRowCountColor; + factory FlowyColorScheme.fromJson(Map json) => _$FlowyColorSchemeFromJson(json); diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index 684e876704..7c3939e5fe 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -63,6 +63,7 @@ class DandelionColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -117,6 +118,7 @@ class DandelionColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 706d7cc3ad..9f0cc50c93 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -3,23 +3,23 @@ import 'package:flutter/material.dart'; import 'colorscheme.dart'; const _white = Color(0xFFFFFFFF); -const _lightHover = Color(0xFFe0f8ff); -const _lightSelector = Color(0xfff2fcff); -const _lightBg1 = Color(0xfff7f8fc); -const _lightBg2 = Color(0xffedeef2); -const _lightShader1 = Color(0xff333333); -const _lightShader3 = Color(0xff828282); -const _lightShader5 = Color(0xffe0e0e0); -const _lightShader6 = Color(0xfff2f2f2); -const _lightMain1 = Color(0xff00bcf0); -const _lightTint9 = Color(0xffe1fbff); -const _darkShader1 = Color(0xff131720); -const _darkShader2 = Color(0xff1A202C); -const _darkShader3 = Color(0xff363D49); -const _darkShader5 = Color(0xffBBC3CD); -const _darkShader6 = Color(0xffF2F2F2); -const _darkMain1 = Color(0xff00BCF0); -const _darkInput = Color(0xff282E3A); +const _lightHover = Color(0xFFe0f8FF); +const _lightSelector = Color(0xFFf2fcFF); +const _lightBg1 = Color(0xFFf7f8fc); +const _lightBg2 = Color(0xFFedeef2); +const _lightShader1 = Color(0xFF333333); +const _lightShader3 = Color(0xFF828282); +const _lightShader5 = Color(0xFFe0e0e0); +const _lightShader6 = Color(0xFFf2f2f2); +const _lightMain1 = Color(0xFF00bcf0); +const _lightTint9 = Color(0xFFe1fbFF); +const _darkShader1 = Color(0xFF131720); +const _darkShader2 = Color(0xFF1A202C); +const _darkShader3 = Color(0xFF363D49); +const _darkShader5 = Color(0xFFBBC3CD); +const _darkShader6 = Color(0xFFF2F2F2); +const _darkMain1 = Color(0xFF00BCF0); +const _darkInput = Color(0xFF282E3A); class DefaultColorScheme extends FlowyColorScheme { const DefaultColorScheme.light() @@ -27,37 +27,38 @@ class DefaultColorScheme extends FlowyColorScheme { surface: _white, hover: _lightHover, selector: _lightSelector, - red: const Color(0xfffb006d), - yellow: const Color(0xffffd667), - green: const Color(0xff66cf80), + red: const Color(0xFFfb006d), + yellow: const Color(0xFFFFd667), + green: const Color(0xFF66cf80), shader1: _lightShader1, - shader2: const Color(0xff4f4f4f), + shader2: const Color(0xFF4f4f4f), shader3: _lightShader3, - shader4: const Color(0xffbdbdbd), + shader4: const Color(0xFFbdbdbd), shader5: _lightShader5, shader6: _lightShader6, shader7: _lightShader1, bg1: _lightBg1, bg2: _lightBg2, - bg3: const Color(0xffe2e4eb), - bg4: const Color(0xff2c144b), - tint1: const Color(0xffe8e0ff), - tint2: const Color(0xffffe7fd), - tint3: const Color(0xffffe7ee), - tint4: const Color(0xffffefe3), - tint5: const Color(0xfffff2cd), - tint6: const Color(0xfff5ffdc), - tint7: const Color(0xffddffd6), - tint8: const Color(0xffdefff1), + bg3: const Color(0xFFe2e4eb), + bg4: const Color(0xFF2c144b), + tint1: const Color(0xFFe8e0FF), + tint2: const Color(0xFFFFe7fd), + tint3: const Color(0xFFFFe7ee), + tint4: const Color(0xFFFFefe3), + tint5: const Color(0xFFFFf2cd), + tint6: const Color(0xFFf5FFdc), + tint7: const Color(0xFFddFFd6), + tint8: const Color(0xFFdeFFf1), tint9: _lightTint9, main1: _lightMain1, - main2: const Color(0xff00b7ea), + main2: const Color(0xFF00b7ea), shadow: const Color.fromRGBO(0, 0, 0, 0.15), sidebarBg: _lightBg1, divider: _lightShader6, topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: const Color(0xFF4f4f4f), input: _white, hint: _lightShader3, primary: _lightMain1, @@ -79,20 +80,20 @@ class DefaultColorScheme extends FlowyColorScheme { surface: _darkShader2, hover: _darkMain1, selector: _darkShader2, - red: const Color(0xfffb006d), - yellow: const Color(0xffF7CF46), - green: const Color(0xff66CF80), + red: const Color(0xFFfb006d), + yellow: const Color(0xFFF7CF46), + green: const Color(0xFF66CF80), shader1: _darkShader1, shader2: _darkShader2, shader3: _darkShader3, - shader4: const Color(0xff505469), + shader4: const Color(0xFF505469), shader5: _darkShader5, shader6: _darkShader6, shader7: _white, - bg1: const Color(0xffF7F8FC), - bg2: const Color(0xffEDEEF2), + bg1: const Color(0xFF1A202C), + bg2: const Color(0xFFEDEEF2), bg3: _darkMain1, - bg4: const Color(0xff2C144B), + bg4: const Color(0xFF2C144B), tint1: const Color(0x4d9327FF), tint2: const Color(0x66FC0088), tint3: const Color(0x4dFC00E2), @@ -103,15 +104,16 @@ class DefaultColorScheme extends FlowyColorScheme { tint8: const Color(0x80008890), tint9: const Color(0x4d0029FF), main1: _darkMain1, - main2: const Color(0xff00B7EA), - shadow: const Color(0xff0F131C), - sidebarBg: const Color(0xff232B38), + main2: const Color(0xFF00B7EA), + shadow: const Color(0xFF0F131C), + sidebarBg: const Color(0xFF232B38), divider: _darkShader3, topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, - hint: const Color(0xff59647a), + hint: const Color(0xFF59647a), primary: _darkMain1, onPrimary: _darkShader1, hoverBG1: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 894fc4c4b2..87fea48cdd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -61,6 +61,7 @@ class LavenderColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightMain1, @@ -113,6 +114,7 @@ class LavenderColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 86a548059e..d2a80f7b92 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -65,6 +65,7 @@ class LemonadeColorScheme extends FlowyColorScheme { topbarBg: _white, icon: _lightShader1, text: _lightShader1, + secondaryText: _lightShader1, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -119,6 +120,7 @@ class LemonadeColorScheme extends FlowyColorScheme { topbarBg: _darkShader1, icon: _darkShader5, text: _darkShader5, + secondaryText: _darkShader5, input: _darkInput, hint: _darkShader5, primary: _darkMain1, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 5aadb7f27e..7ee3c77a01 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -2,34 +2,8 @@ import 'package:flutter/material.dart'; @immutable class AFThemeExtension extends ThemeExtension { - final Color? warning; - final Color? success; - - final Color tint1; - final Color tint2; - final Color tint3; - final Color tint4; - final Color tint5; - final Color tint6; - final Color tint7; - final Color tint8; - final Color tint9; - - final Color textColor; - final Color greyHover; - final Color greySelect; - final Color lightGreyHover; - final Color toggleOffFill; - final Color progressBarBGColor; - final Color toggleButtonBGColor; - final Color calloutBGColor; - final Color tableCellBGColor; - final Color calendarWeekendBGColor; - final Color gridRowCountColor; - - final TextStyle code; - final TextStyle callout; - final TextStyle caption; + static AFThemeExtension of(BuildContext context) => + Theme.of(context).extension()!; const AFThemeExtension({ required this.warning, @@ -48,6 +22,7 @@ class AFThemeExtension extends ThemeExtension { required this.lightGreyHover, required this.toggleOffFill, required this.textColor, + required this.secondaryTextColor, required this.calloutBGColor, required this.tableCellBGColor, required this.calendarWeekendBGColor, @@ -59,9 +34,35 @@ class AFThemeExtension extends ThemeExtension { required this.gridRowCountColor, }); - static AFThemeExtension of(BuildContext context) { - return Theme.of(context).extension()!; - } + final Color? warning; + final Color? success; + + final Color tint1; + final Color tint2; + final Color tint3; + final Color tint4; + final Color tint5; + final Color tint6; + final Color tint7; + final Color tint8; + final Color tint9; + + final Color textColor; + final Color secondaryTextColor; + final Color greyHover; + final Color greySelect; + final Color lightGreyHover; + final Color toggleOffFill; + final Color progressBarBGColor; + final Color toggleButtonBGColor; + final Color calloutBGColor; + final Color tableCellBGColor; + final Color calendarWeekendBGColor; + final Color gridRowCountColor; + + final TextStyle code; + final TextStyle callout; + final TextStyle caption; @override AFThemeExtension copyWith({ @@ -77,6 +78,7 @@ class AFThemeExtension extends ThemeExtension { Color? tint8, Color? tint9, Color? textColor, + Color? secondaryTextColor, Color? calloutBGColor, Color? tableCellBGColor, Color? greyHover, @@ -90,36 +92,36 @@ class AFThemeExtension extends ThemeExtension { TextStyle? code, TextStyle? callout, TextStyle? caption, - }) { - return AFThemeExtension( - warning: warning ?? this.warning, - success: success ?? this.success, - tint1: tint1 ?? this.tint1, - tint2: tint2 ?? this.tint2, - tint3: tint3 ?? this.tint3, - tint4: tint4 ?? this.tint4, - tint5: tint5 ?? this.tint5, - tint6: tint6 ?? this.tint6, - tint7: tint7 ?? this.tint7, - tint8: tint8 ?? this.tint8, - tint9: tint9 ?? this.tint9, - textColor: textColor ?? this.textColor, - calloutBGColor: calloutBGColor ?? this.calloutBGColor, - tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor, - greyHover: greyHover ?? this.greyHover, - greySelect: greySelect ?? this.greySelect, - lightGreyHover: lightGreyHover ?? this.lightGreyHover, - toggleOffFill: toggleOffFill ?? this.toggleOffFill, - progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor, - toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor, - calendarWeekendBGColor: - calendarWeekendBGColor ?? this.calendarWeekendBGColor, - gridRowCountColor: gridRowCountColor ?? this.gridRowCountColor, - code: code ?? this.code, - callout: callout ?? this.callout, - caption: caption ?? this.caption, - ); - } + }) => + AFThemeExtension( + warning: warning ?? this.warning, + success: success ?? this.success, + tint1: tint1 ?? this.tint1, + tint2: tint2 ?? this.tint2, + tint3: tint3 ?? this.tint3, + tint4: tint4 ?? this.tint4, + tint5: tint5 ?? this.tint5, + tint6: tint6 ?? this.tint6, + tint7: tint7 ?? this.tint7, + tint8: tint8 ?? this.tint8, + tint9: tint9 ?? this.tint9, + textColor: textColor ?? this.textColor, + secondaryTextColor: secondaryTextColor ?? this.secondaryTextColor, + calloutBGColor: calloutBGColor ?? this.calloutBGColor, + tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor, + greyHover: greyHover ?? this.greyHover, + greySelect: greySelect ?? this.greySelect, + lightGreyHover: lightGreyHover ?? this.lightGreyHover, + toggleOffFill: toggleOffFill ?? this.toggleOffFill, + progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor, + toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor, + calendarWeekendBGColor: + calendarWeekendBGColor ?? this.calendarWeekendBGColor, + gridRowCountColor: gridRowCountColor ?? this.gridRowCountColor, + code: code ?? this.code, + callout: callout ?? this.callout, + caption: caption ?? this.caption, + ); @override ThemeExtension lerp( @@ -140,6 +142,11 @@ class AFThemeExtension extends ThemeExtension { tint8: Color.lerp(tint8, other.tint8, t)!, tint9: Color.lerp(tint9, other.tint9, t)!, textColor: Color.lerp(textColor, other.textColor, t)!, + secondaryTextColor: Color.lerp( + secondaryTextColor, + other.secondaryTextColor, + t, + )!, calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!, tableCellBGColor: Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!, @@ -188,51 +195,29 @@ enum FlowyTint { orElse: () => FlowyTint.tint1, ); } + + Color color(BuildContext context) => switch (this) { + FlowyTint.tint1 => AFThemeExtension.of(context).tint1, + FlowyTint.tint2 => AFThemeExtension.of(context).tint2, + FlowyTint.tint3 => AFThemeExtension.of(context).tint3, + FlowyTint.tint4 => AFThemeExtension.of(context).tint4, + FlowyTint.tint5 => AFThemeExtension.of(context).tint5, + FlowyTint.tint6 => AFThemeExtension.of(context).tint6, + FlowyTint.tint7 => AFThemeExtension.of(context).tint7, + FlowyTint.tint8 => AFThemeExtension.of(context).tint8, + FlowyTint.tint9 => AFThemeExtension.of(context).tint9, + }; - Color color(BuildContext context) { - switch (this) { - case FlowyTint.tint1: - return AFThemeExtension.of(context).tint1; - case FlowyTint.tint2: - return AFThemeExtension.of(context).tint2; - case FlowyTint.tint3: - return AFThemeExtension.of(context).tint3; - case FlowyTint.tint4: - return AFThemeExtension.of(context).tint4; - case FlowyTint.tint5: - return AFThemeExtension.of(context).tint5; - case FlowyTint.tint6: - return AFThemeExtension.of(context).tint6; - case FlowyTint.tint7: - return AFThemeExtension.of(context).tint7; - case FlowyTint.tint8: - return AFThemeExtension.of(context).tint8; - case FlowyTint.tint9: - return AFThemeExtension.of(context).tint9; - } - } - - String get id { - switch (this) { - // DON'T change this name because it's saved in the database! - case FlowyTint.tint1: - return 'appflowy_them_color_tint1'; - case FlowyTint.tint2: - return 'appflowy_them_color_tint2'; - case FlowyTint.tint3: - return 'appflowy_them_color_tint3'; - case FlowyTint.tint4: - return 'appflowy_them_color_tint4'; - case FlowyTint.tint5: - return 'appflowy_them_color_tint5'; - case FlowyTint.tint6: - return 'appflowy_them_color_tint6'; - case FlowyTint.tint7: - return 'appflowy_them_color_tint7'; - case FlowyTint.tint8: - return 'appflowy_them_color_tint8'; - case FlowyTint.tint9: - return 'appflowy_them_color_tint9'; - } - } + String get id => switch (this) { + // DON'T change this name because it's saved in the database! + FlowyTint.tint1 => 'appflowy_them_color_tint1', + FlowyTint.tint2 => 'appflowy_them_color_tint2', + FlowyTint.tint3 => 'appflowy_them_color_tint3', + FlowyTint.tint4 => 'appflowy_them_color_tint4', + FlowyTint.tint5 => 'appflowy_them_color_tint5', + FlowyTint.tint6 => 'appflowy_them_color_tint6', + FlowyTint.tint7 => 'appflowy_them_color_tint7', + FlowyTint.tint8 => 'appflowy_them_color_tint8', + FlowyTint.tint9 => 'appflowy_them_color_tint9', + }; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index dced70a6be..832eca88e0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -146,9 +145,32 @@ class FlowyButton extends StatelessWidget { } class FlowyTextButton extends StatelessWidget { + const FlowyTextButton( + this.text, { + super.key, + this.onPressed, + this.fontSize, + this.fontColor, + this.fontHoverColor, + this.overflow = TextOverflow.ellipsis, + this.fontWeight, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + this.hoverColor, + this.fillColor, + this.heading, + this.radius, + this.mainAxisAlignment = MainAxisAlignment.start, + this.tooltip, + this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0), + this.decoration, + this.fontFamily, + this.isDangerous = false, + }); + final String text; final FontWeight? fontWeight; final Color? fontColor; + final Color? fontHoverColor; final double? fontSize; final TextOverflow overflow; @@ -165,27 +187,7 @@ class FlowyTextButton extends StatelessWidget { final TextDecoration? decoration; final String? fontFamily; - - // final HoverDisplayConfig? hoverDisplay; - const FlowyTextButton( - this.text, { - super.key, - this.onPressed, - this.fontSize, - this.fontColor, - this.overflow = TextOverflow.ellipsis, - this.fontWeight, - this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - this.hoverColor, - this.fillColor, - this.heading, - this.radius, - this.mainAxisAlignment = MainAxisAlignment.start, - this.tooltip, - this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0), - this.decoration, - this.fontFamily, - }); + final bool isDangerous; @override Widget build(BuildContext context) { @@ -194,18 +196,7 @@ class FlowyTextButton extends StatelessWidget { children.add(heading!); children.add(const HSpace(8)); } - children.add( - FlowyText( - text, - overflow: overflow, - fontWeight: fontWeight, - fontSize: fontSize, - color: fontColor, - textAlign: TextAlign.center, - decoration: decoration, - fontFamily: fontFamily, - ), - ); + children.add(Text(text, overflow: overflow, textAlign: TextAlign.center)); Widget child = Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -213,30 +204,67 @@ class FlowyTextButton extends StatelessWidget { children: children, ); - child = RawMaterialButton( - focusNode: FocusNode(skipTraversal: onPressed == null), - hoverElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), - fillColor: fillColor ?? Theme.of(context).colorScheme.secondaryContainer, - hoverColor: - hoverColor ?? Theme.of(context).colorScheme.secondaryContainer, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, + child = ConstrainedBox( constraints: constraints, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: padding, - onPressed: onPressed, - child: child, + child: TextButton( + onPressed: onPressed ?? () {}, + focusNode: FocusNode(skipTraversal: onPressed == null), + style: ButtonStyle( + overlayColor: const MaterialStatePropertyAll(Colors.transparent), + splashFactory: NoSplash.splashFactory, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: MaterialStateProperty.all(padding), + elevation: MaterialStateProperty.all(0), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + side: BorderSide( + color: isDangerous + ? Theme.of(context).colorScheme.error + : Colors.transparent, + ), + borderRadius: radius ?? Corners.s6Border, + ), + ), + textStyle: MaterialStateProperty.all( + TextStyle( + fontWeight: fontWeight ?? FontWeight.w500, + fontSize: fontSize, + decoration: decoration, + fontFamily: fontFamily, + ), + ), + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.hovered)) { + return hoverColor ?? + (isDangerous + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary); + } + + return fillColor ?? + (isDangerous + ? Colors.transparent + : Theme.of(context).colorScheme.secondaryContainer); + }, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.hovered)) { + return fontHoverColor ?? + (fontColor ?? Theme.of(context).colorScheme.onSurface); + } + + return fontColor ?? Theme.of(context).colorScheme.onSurface; + }, + ), + ), + child: child, + ), ); if (tooltip != null) { - child = FlowyTooltip( - message: tooltip!, - child: child, - ); + child = FlowyTooltip(message: tooltip!, child: child); } if (onPressed == null) { @@ -248,22 +276,6 @@ class FlowyTextButton extends StatelessWidget { } class FlowyRichTextButton extends StatelessWidget { - final InlineSpan text; - final TextOverflow overflow; - - final VoidCallback? onPressed; - final EdgeInsets padding; - final Widget? heading; - final Color? hoverColor; - final Color? fillColor; - final BorderRadius? radius; - final MainAxisAlignment mainAxisAlignment; - final String? tooltip; - final BoxConstraints constraints; - - final TextDecoration? decoration; - - // final HoverDisplayConfig? hoverDisplay; const FlowyRichTextButton( this.text, { super.key, @@ -280,6 +292,21 @@ class FlowyRichTextButton extends StatelessWidget { this.decoration, }); + final InlineSpan text; + final TextOverflow overflow; + + final VoidCallback? onPressed; + final EdgeInsets padding; + final Widget? heading; + final Color? hoverColor; + final Color? fillColor; + final BorderRadius? radius; + final MainAxisAlignment mainAxisAlignment; + final String? tooltip; + final BoxConstraints constraints; + + final TextDecoration? decoration; + @override Widget build(BuildContext context) { List children = []; @@ -288,11 +315,7 @@ class FlowyRichTextButton extends StatelessWidget { children.add(const HSpace(6)); } children.add( - RichText( - text: text, - overflow: overflow, - textAlign: TextAlign.center, - ), + RichText(text: text, overflow: overflow, textAlign: TextAlign.center), ); Widget child = Padding( @@ -320,16 +343,10 @@ class FlowyRichTextButton extends StatelessWidget { child: child, ); - child = IgnoreParentGestureWidget( - onPress: onPressed, - child: child, - ); + child = IgnoreParentGestureWidget(onPress: onPressed, child: child); if (tooltip != null) { - child = FlowyTooltip( - message: tooltip!, - child: child, - ); + child = FlowyTooltip(message: tooltip!, child: child); } return child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 8e2cb9ddad..d26f353c72 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_infra/size.dart'; + class FlowyTextField extends StatefulWidget { final String? hintText; final String? text; @@ -34,6 +35,8 @@ class FlowyTextField extends StatefulWidget { final TextInputAction? textInputAction; final TextInputType? keyboardType; final List? inputFormatters; + final bool obscureText; + final bool isDense; const FlowyTextField({ super.key, @@ -66,6 +69,8 @@ class FlowyTextField extends StatefulWidget { this.textInputAction, this.keyboardType = TextInputType.multiline, this.inputFormatters, + this.obscureText = false, + this.isDense = true, }); @override @@ -159,6 +164,7 @@ class FlowyTextFieldState extends State { textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center, keyboardType: widget.keyboardType, inputFormatters: widget.inputFormatters, + obscureText: widget.obscureText, decoration: widget.decoration ?? InputDecoration( constraints: widget.hintTextConstraints ?? @@ -166,16 +172,15 @@ class FlowyTextFieldState extends State { maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58, ), contentPadding: EdgeInsets.symmetric( - horizontal: 12, + horizontal: widget.isDense ? 12 : 18, vertical: (widget.maxLines == null || widget.maxLines! > 1) ? 12 : 0, ), enabledBorder: OutlineInputBorder( + borderRadius: Corners.s8Border, borderSide: BorderSide( color: Theme.of(context).colorScheme.outline, - width: 1.0, ), - borderRadius: Corners.s8Border, ), isDense: false, hintText: widget.hintText, @@ -193,23 +198,20 @@ class FlowyTextFieldState extends State { suffixText: widget.showCounter ? _suffixText() : "", counterText: "", focusedBorder: OutlineInputBorder( + borderRadius: Corners.s8Border, borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, - width: 1.0, ), - borderRadius: Corners.s8Border, ), errorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, - width: 1.0, ), borderRadius: Corners.s8Border, ), focusedErrorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, - width: 1.0, ), borderRadius: Corners.s8Border, ), diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 5ec4972890..36115b5f53 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -255,4 +255,4 @@ flutter: - assets/template/ - assets/test/workspaces/markdowns/ - assets/test/workspaces/database/ - # END: EXCLUDE_IN_RELEASE \ No newline at end of file + # END: EXCLUDE_IN_RELEASE diff --git a/frontend/appflowy_flutter/test/widget_test/workspace/settings/settings_customize_shortcuts_view_test.dart b/frontend/appflowy_flutter/test/widget_test/workspace/settings/settings_customize_shortcuts_view_test.dart deleted file mode 100644 index 8d23dabf3e..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/workspace/settings/settings_customize_shortcuts_view_test.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -// ignore: depend_on_referenced_packages -import 'package:mocktail/mocktail.dart'; - -class MockShortcutsCubit extends MockCubit - implements ShortcutsCubit {} - -void main() { - group( - "CustomizeShortcutsView", - () { - group( - "should be displayed in ViewState", - () { - late ShortcutsCubit mockShortcutsCubit; - - setUp(() { - mockShortcutsCubit = MockShortcutsCubit(); - }); - - testWidgets('Initial when cubit emits [ShortcutsStatus.Initial]', - (widgetTester) async { - when(() => mockShortcutsCubit.state) - .thenReturn(const ShortcutsState()); - - await widgetTester.pumpWidget( - BlocProvider.value( - value: mockShortcutsCubit, - child: - const MaterialApp(home: SettingsCustomizeShortcutsView()), - ), - ); - expect(find.byType(CircularProgressIndicator), findsOneWidget); - }); - - testWidgets( - 'Updating when cubit emits [ShortcutsStatus.updating]', - (widgetTester) async { - when(() => mockShortcutsCubit.state).thenReturn( - const ShortcutsState(status: ShortcutsStatus.updating), - ); - - await widgetTester.pumpWidget( - BlocProvider.value( - value: mockShortcutsCubit, - child: - const MaterialApp(home: SettingsCustomizeShortcutsView()), - ), - ); - expect(find.byType(CircularProgressIndicator), findsOneWidget); - }, - ); - - testWidgets( - 'Shows ShortcutsList when cubit emits [ShortcutsStatus.success]', - (widgetTester) async { - KeyEventResult dummyHandler(EditorState e) => - KeyEventResult.handled; - - final dummyShortcuts = [ - CommandShortcutEvent( - key: 'Copy', - getDescription: () => 'Copy', - command: 'ctrl+c', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Paste', - getDescription: () => 'Paste', - command: 'ctrl+v', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Undo', - getDescription: () => 'Undo', - command: 'ctrl+z', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Redo', - getDescription: () => 'Redo', - command: 'ctrl+y', - handler: dummyHandler, - ), - ]; - - when(() => mockShortcutsCubit.state).thenReturn( - ShortcutsState( - status: ShortcutsStatus.success, - commandShortcutEvents: dummyShortcuts, - ), - ); - await widgetTester.pumpWidget( - BlocProvider.value( - value: mockShortcutsCubit, - child: - const MaterialApp(home: SettingsCustomizeShortcutsView()), - ), - ); - - await widgetTester.pump(); - - final listViewFinder = find.byType(ShortcutsListView); - final foundShortcuts = widgetTester - .widget(listViewFinder) - .shortcuts; - - expect(listViewFinder, findsOneWidget); - expect(foundShortcuts, dummyShortcuts); - }, - ); - - testWidgets('Shows Error when cubit emits [ShortcutsStatus.failure]', - (tester) async { - when(() => mockShortcutsCubit.state).thenReturn( - const ShortcutsState( - status: ShortcutsStatus.failure, - ), - ); - await tester.pumpWidget( - BlocProvider.value( - value: mockShortcutsCubit, - child: - const MaterialApp(home: SettingsCustomizeShortcutsView()), - ), - ); - expect(find.byType(ShortcutsErrorView), findsOneWidget); - }); - }, - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_error_view_test.dart b/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_error_view_test.dart deleted file mode 100644 index 31cc7ec53e..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_error_view_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group("ShortcutsErrorView", () { - testWidgets("displays correctly", (widgetTester) async { - await widgetTester.pumpWidget( - const MaterialApp( - home: ShortcutsErrorView( - errorMessage: 'Error occured', - ), - ), - ); - - expect(find.byType(FlowyText), findsOneWidget); - expect(find.byType(FlowyIconButton), findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart b/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart deleted file mode 100644 index f4bfbc7c1a..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:appflowy_editor/appflowy_editor.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'; - -void main() { - KeyEventResult dummyHandler(EditorState e) => KeyEventResult.handled; - - final shortcut = CommandShortcutEvent( - key: 'Copy', - getDescription: () => 'Copy', - command: 'ctrl+c', - handler: dummyHandler, - ); - - group("ShortcutsListTile", () { - group( - "should be displayed correctly", - () { - testWidgets('with key and command', (widgetTester) async { - final sKey = Key(shortcut.key); - - await widgetTester.pumpWidget( - MaterialApp( - home: ShortcutsListTile(shortcutEvent: shortcut), - ), - ); - - final commandTextFinder = find.byKey(sKey); - final foundCommand = - widgetTester.widget(commandTextFinder).text; - - expect(commandTextFinder, findsOneWidget); - expect(foundCommand, shortcut.key); - - final btnFinder = find.byType(FlowyTextButton); - final foundBtnText = - widgetTester.widget(btnFinder).text; - - expect(btnFinder, findsOneWidget); - expect(foundBtnText, shortcut.command); - }); - }, - ); - - group( - "taps the button", - () { - testWidgets("opens AlertDialog correctly", (widgetTester) async { - await widgetTester.pumpWidget( - MaterialApp( - home: ShortcutsListTile(shortcutEvent: shortcut), - ), - ); - - final btnFinder = find.byType(FlowyTextButton); - final foundBtnText = - widgetTester.widget(btnFinder).text; - - expect(btnFinder, findsOneWidget); - expect(foundBtnText, shortcut.command); - - await widgetTester.tap(btnFinder); - await widgetTester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - expect(find.byType(KeyboardListener), findsOneWidget); - }); - - testWidgets("updates the text with new key event", - (widgetTester) async { - await widgetTester.pumpWidget( - MaterialApp( - home: ShortcutsListTile(shortcutEvent: shortcut), - ), - ); - - final btnFinder = find.byType(FlowyTextButton); - - await widgetTester.tap(btnFinder); - await widgetTester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - expect(find.byType(KeyboardListener), findsOneWidget); - - await widgetTester.sendKeyEvent(LogicalKeyboardKey.keyC); - - expect(find.text('c'), findsOneWidget); - }); - }, - ); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_view_test.dart b/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_view_test.dart deleted file mode 100644 index ed1097a64b..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_view_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - KeyEventResult dummyHandler(EditorState e) => KeyEventResult.handled; - - final dummyShortcuts = [ - CommandShortcutEvent( - key: 'Copy', - getDescription: () => 'Copy', - command: 'ctrl+c', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Paste', - getDescription: () => 'Paste', - command: 'ctrl+v', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Undo', - getDescription: () => 'Undo', - command: 'ctrl+z', - handler: dummyHandler, - ), - CommandShortcutEvent( - key: 'Redo', - getDescription: () => 'Redo', - command: 'ctrl+y', - handler: dummyHandler, - ), - ]; - - group("ShortcutsListView", () { - group("should be displayed correctly", () { - testWidgets("with empty shortcut list", (widgetTester) async { - await widgetTester.pumpWidget( - const MaterialApp( - home: ShortcutsListView(shortcuts: []), - ), - ); - - expect(find.byType(FlowyText), findsNWidgets(3)); - //we expect three text widgets which are keybinding, command, and reset - expect(find.byType(ListView), findsOneWidget); - expect(find.byType(ShortcutsListTile), findsNothing); - }); - - testWidgets("with 1 item in shortcut list", (widgetTester) async { - await widgetTester.pumpWidget( - MaterialApp( - home: ShortcutsListView(shortcuts: [dummyShortcuts[0]]), - ), - ); - - await widgetTester.pumpAndSettle(); - - expect(find.byType(FlowyText), findsAtLeastNWidgets(3)); - expect(find.byType(ListView), findsOneWidget); - expect(find.byType(ShortcutsListTile), findsOneWidget); - }); - - testWidgets("with populated shortcut list", (widgetTester) async { - await widgetTester.pumpWidget( - MaterialApp( - home: ShortcutsListView(shortcuts: dummyShortcuts), - ), - ); - - expect(find.byType(FlowyText), findsAtLeastNWidgets(3)); - expect(find.byType(ListView), findsOneWidget); - expect( - find.byType(ShortcutsListTile), - findsNWidgets(dummyShortcuts.length), - ); - }); - }); - }); -} diff --git a/frontend/resources/flowy_icons/24x/arrow_back.svg b/frontend/resources/flowy_icons/24x/arrow_back.svg new file mode 100644 index 0000000000..ff56023329 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/arrow_back.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_account.svg b/frontend/resources/flowy_icons/24x/settings_account.svg new file mode 100644 index 0000000000..c64b21ec04 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_account.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/settings_data.svg b/frontend/resources/flowy_icons/24x/settings_data.svg new file mode 100644 index 0000000000..0d0934cb52 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_data.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_members.svg b/frontend/resources/flowy_icons/24x/settings_members.svg new file mode 100644 index 0000000000..ad970d4709 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_members.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_notifications.svg b/frontend/resources/flowy_icons/24x/settings_notifications.svg new file mode 100644 index 0000000000..5fc6d58d55 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_notifications.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_plan.svg b/frontend/resources/flowy_icons/24x/settings_plan.svg new file mode 100644 index 0000000000..5c6f53f836 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_plan.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_shortcuts.svg b/frontend/resources/flowy_icons/24x/settings_shortcuts.svg new file mode 100644 index 0000000000..aa2765c1f7 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_shortcuts.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_sync.svg b/frontend/resources/flowy_icons/24x/settings_sync.svg new file mode 100644 index 0000000000..bdd0b49356 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_sync.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/settings_workplace.svg b/frontend/resources/flowy_icons/24x/settings_workplace.svg new file mode 100644 index 0000000000..4c9b4d9bad --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_workplace.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ba5fa165ab..318908b76c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -253,7 +253,8 @@ "editContact": "Edit Contact" }, "button": { - "ok": "OK", + "ok": "Ok", + "confirm": "Confirm", "done": "Done", "cancel": "Cancel", "signIn": "Sign In", @@ -316,6 +317,35 @@ }, "settings": { "title": "Settings", + "accountPage": { + "menuLabel": "My account", + "title": "My account", + "description": "Customize your profile, manage account security, open AI keys, or login into your account.", + "general": { + "title": "Account name & profile image", + "changeProfilePicture": "Change" + }, + "email": { + "title": "Email", + "actions": { + "change": "Change email" + } + }, + "keys": { + "title": "AI Keys", + "openAILabel": "OpenAI key", + "openAITooltip": "The OpenAPI API key to use for the AI models", + "openAIHint": "Input your OpenAI API Key", + "stabilityAILabel": "StabilityAI key", + "stabilityAITooltip": "The Stability API key to use for the AI models", + "stabilityAIHint": "Input your Stability API Key" + }, + "login": { + "title": "Account login", + "loginLabel": "Log in", + "logoutLabel": "Log out" + } + }, "menu": { "appearance": "Appearance", "language": "Language", @@ -1503,4 +1533,4 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} \ No newline at end of file +}