From 4a126e17ce34ccac4195e2d1dcc802a8dc356229 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:30:19 +0200 Subject: [PATCH] fix: settings improvements (#5547) * fix: user workspace bloc changed * fix: use translate card cell style * fix: add getworkspacemember * fix: billing launch review * fix: disable time field * fix: member tooltip * fix: remove my account description * fix: punctuation * fix: filter workspace font * fix: cloud toggle * fix: minor adjustments * chore: disable cloud document search * fix: improve workspace name textfield * test: move billing test to cloud * fix: use cache over remote * fix: clippy and tests * chore: amend flowy tooltip * test: add pump and settle * test: integration test for local auth --- .../integration_test/cloud/cloud_runner.dart | 6 - .../desktop/database/database_row_test.dart | 17 +- .../settings/settings_billing_test.dart | 50 ++ .../desktop/settings/settings_runner.dart | 2 + .../shared/auth_operation.dart | 13 +- .../shared/database_test_op.dart | 6 +- .../field/mobile_field_bottom_sheets.dart | 5 +- .../application/setting/group_bloc.dart | 4 +- .../widgets/filter/filter_menu_item.dart | 11 +- .../calendar_card_cell_style.dart | 8 +- .../desktop_board_card_cell_style.dart | 3 +- .../mobile_board_card_cell_style.dart | 3 +- .../widgets/field/field_type_list.dart | 2 +- .../widgets/group/database_group.dart | 7 +- .../lib/user/application/user_service.dart | 27 +- .../command_palette/command_palette_bloc.dart | 2 +- .../appearance/desktop_appearance.dart | 4 +- .../appearance/mobile_appearance.dart | 4 +- .../settings/plan/settings_plan_bloc.dart | 8 +- .../plan/workspace_subscription_ext.dart | 10 + .../workspace/workspace_settings_bloc.dart | 9 +- .../application/user/user_workspace_bloc.dart | 23 +- .../menu/sidebar/shared/sidebar_setting.dart | 43 +- .../settings/pages/settings_account_view.dart | 1 - .../settings/pages/settings_billing_view.dart | 15 +- .../settings_plan_comparison_dialog.dart | 281 +++++++---- .../settings/pages/settings_plan_view.dart | 36 +- .../pages/settings_workspace_view.dart | 449 ++++++++++-------- .../settings/settings_dialog.dart | 33 +- .../shared/flowy_gradient_button.dart | 2 +- .../settings/shared/settings_dropdown.dart | 14 +- .../settings/shared/settings_input_field.dart | 12 +- .../members/workspace_member_page.dart | 2 + .../widgets/setting_appflowy_cloud.dart | 24 +- .../settings/widgets/settings_menu.dart | 9 +- .../lib/colorscheme/colorscheme.dart | 2 + .../lib/colorscheme/dandelion.dart | 2 + .../lib/colorscheme/default_colorscheme.dart | 2 + .../flowy_infra/lib/colorscheme/lavender.dart | 2 + .../flowy_infra/lib/colorscheme/lemonade.dart | 2 + .../flowy_infra/lib/theme_extension.dart | 9 + .../flowy_infra_ui/lib/style_widget/text.dart | 1 + .../lib/widget/flowy_tooltip.dart | 4 + frontend/resources/flowy_icons/24x/check.svg | 8 + frontend/resources/translations/en.json | 32 +- .../src/folder_event.rs | 2 +- .../src/deps_resolve/search_deps.rs | 10 +- .../af_cloud/impls/user/cloud_service_impl.rs | 17 + frontend/rust-lib/flowy-user-pub/src/cloud.rs | 8 + .../rust-lib/flowy-user/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-user/src/event_map.rs | 4 +- .../user_manager/manager_user_workspace.rs | 15 +- 52 files changed, 857 insertions(+), 410 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart create mode 100644 frontend/resources/flowy_icons/24x/check.svg diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart index c66cdd5cc1..3a72f012d6 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart @@ -1,7 +1,6 @@ import 'anon_user_continue_test.dart' as anon_user_continue_test; import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; import 'empty_test.dart' as preset_af_cloud_env_test; -// import 'document_sync_test.dart' as document_sync_test; import 'user_setting_sync_test.dart' as user_sync_test; import 'workspace/change_name_and_icon_test.dart' as change_workspace_name_and_icon_test; @@ -10,13 +9,8 @@ import 'workspace/collaborative_workspace_test.dart' Future main() async { preset_af_cloud_env_test.main(); - appflowy_cloud_auth_test.main(); - - // document_sync_test.main(); - user_sync_test.main(); - anon_user_continue_test.main(); // workspace diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart index 6721de2d16..9865a47a6e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart @@ -41,15 +41,16 @@ void main() { await tester.tapAnonymousSignInButton(); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.hoverOnFirstRowOfGrid(); + await tester.hoverOnFirstRowOfGrid(() async { + // Open the row menu and then click the delete + await tester.tapRowMenuButtonInGrid(); + await tester.pumpAndSettle(); + await tester.tapDeleteOnRowMenu(); + await tester.pumpAndSettle(); - // Open the row menu and then click the delete - await tester.tapRowMenuButtonInGrid(); - await tester.tapDeleteOnRowMenu(); - - // 3 initial rows - 1 deleted - await tester.assertNumberOfRowsInGridPage(2); - await tester.pumpAndSettle(); + // 3 initial rows - 1 deleted + await tester.assertNumberOfRowsInGridPage(2); + }); }); testWidgets('check number of row indicator in the initial grid', diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart new file mode 100644 index 0000000000..a311eb8377 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart @@ -0,0 +1,50 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/auth_operation.dart'; +import '../../shared/base.dart'; +import '../../shared/expectation.dart'; +import '../../shared/settings.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Settings Billing', () { + testWidgets('Local auth cannot see plan+billing', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapSignInAsGuest(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + await tester.openSettings(); + await tester.pumpAndSettle(); + + // We check that another settings page is present to ensure + // it's not a fluke + expect( + find.text( + LocaleKeys.settings_workspacePage_menuLabel.tr(), + skipOffstage: false, + ), + findsOneWidget, + ); + + expect( + find.text( + LocaleKeys.settings_planPage_menuLabel.tr(), + skipOffstage: false, + ), + findsNothing, + ); + + expect( + find.text( + LocaleKeys.settings_billingPage_menuLabel.tr(), + skipOffstage: false, + ), + findsNothing, + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart index d76d41cd80..fa22f1c740 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart @@ -1,9 +1,11 @@ import 'package:integration_test/integration_test.dart'; import 'notifications_settings_test.dart' as notifications_settings_test; +import 'settings_billing_test.dart' as settings_billing_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); notifications_settings_test.main(); + settings_billing_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart index 873e6244b6..9e205182a4 100644 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart @@ -5,6 +5,7 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -44,6 +45,12 @@ extension AppFlowyAuthTest on WidgetTester { assert(isSwitched == value); } + void assertToggleValue(Finder finder, bool value) { + final Toggle switchWidget = widget(finder); + final isSwitched = switchWidget.value; + assert(isSwitched == value); + } + void assertEnableEncryptSwitchValue(bool value) { assertSwitchValue( find.descendant( @@ -65,10 +72,10 @@ extension AppFlowyAuthTest on WidgetTester { } void assertAppFlowyCloudEnableSyncSwitchValue(bool value) { - assertSwitchValue( + assertToggleValue( find.descendant( of: find.byType(AppFlowyCloudEnableSync), - matching: find.byWidgetPredicate((widget) => widget is Switch), + matching: find.byWidgetPredicate((widget) => widget is Toggle), ), value, ); @@ -86,7 +93,7 @@ extension AppFlowyAuthTest on WidgetTester { Future toggleEnableSync(Type syncButton) async { final finder = find.descendant( of: find.byType(syncButton), - matching: find.byWidgetPredicate((widget) => widget is Switch), + matching: find.byWidgetPredicate((widget) => widget is Toggle), ); await tapButton(finder); 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 8277b4db97..754e80342c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -130,12 +130,12 @@ extension AppFlowyDatabaseTest on WidgetTester { await openPage('v020', layout: ViewLayoutPB.Grid); } - Future hoverOnFirstRowOfGrid() async { + Future hoverOnFirstRowOfGrid([Future Function()? onHover]) async { final findRow = find.byType(GridRow); expect(findRow, findsWidgets); final firstRow = findRow.first; - await hoverOnWidget(firstRow); + await hoverOnWidget(firstRow, onHover: onHover); } Future editCell({ @@ -876,11 +876,13 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future tapRowMenuButtonInGrid() async { + expect(find.byType(RowMenuButton), findsOneWidget); await tapButton(find.byType(RowMenuButton)); } /// Should call [tapRowMenuButtonInGrid] first. Future tapDeleteOnRowMenu() async { + expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget); await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index 7d81801d73..fb1bda724d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -8,7 +10,6 @@ import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'mobile_create_field_screen.dart'; @@ -28,7 +29,7 @@ const mobileSupportedFieldTypes = [ FieldType.CreatedTime, FieldType.Checkbox, FieldType.Checklist, - FieldType.Time, + // FieldType.Time, ]; Future showFieldTypeGridBottomSheet( diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart index 7b28a696ee..c62e30b742 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart @@ -49,9 +49,7 @@ class DatabaseGroupBloc extends Bloc { on( (event, emit) async { await event.when( - initial: () { - _startListening(); - }, + initial: () async => _startListening(), didReceiveFieldUpdate: (fieldInfos) { emit( state.copyWith( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart index 7ce6b5a223..ee9f168130 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart @@ -1,6 +1,7 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; + import 'choicechip/checkbox.dart'; import 'choicechip/checklist/checklist.dart'; import 'choicechip/date.dart'; @@ -8,7 +9,6 @@ import 'choicechip/number.dart'; import 'choicechip/select_option/select_option.dart'; import 'choicechip/text.dart'; import 'choicechip/url.dart'; -import 'choicechip/time.dart'; import 'filter_info.dart'; class FilterMenuItem extends StatelessWidget { @@ -23,15 +23,14 @@ class FilterMenuItem extends StatelessWidget { FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo), FieldType.MultiSelect => SelectOptionFilterChoicechip(filterInfo: filterInfo), - FieldType.Number => - NumberFilterChoiceChip(filterInfo: filterInfo), + FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo), FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), FieldType.SingleSelect => SelectOptionFilterChoicechip(filterInfo: filterInfo), FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo), FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), - FieldType.Time => - TimeFilterChoiceChip(filterInfo: filterInfo), + // FieldType.Time => + // TimeFilterChoiceChip(filterInfo: filterInfo), _ => const SizedBox(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart index 785c1f917a..df7bb72f60 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart @@ -1,7 +1,9 @@ -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; + import '../card_cell_builder.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart'; import '../card_cell_skeleton/checklist_card_cell.dart'; @@ -84,7 +86,7 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Translate: SummaryCardCellStyle( + FieldType.Translate: TranslateCardCellStyle( padding: padding, textStyle: textStyle, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index 95b7baa494..7fcb289c1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../card_cell_builder.dart'; @@ -90,7 +91,7 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Translate: SummaryCardCellStyle( + FieldType.Translate: TranslateCardCellStyle( padding: padding, textStyle: textStyle, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart index 952d20e7e5..3911678176 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../card_cell_builder.dart'; @@ -89,7 +90,7 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Translate: SummaryCardCellStyle( + FieldType.Translate: TranslateCardCellStyle( padding: padding, textStyle: textStyle, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index 94ed2d8405..84d4c49177 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -22,7 +22,7 @@ const List _supportedFieldTypes = [ FieldType.CreatedTime, FieldType.Relation, FieldType.Summary, - FieldType.Time, + // FieldType.Time, FieldType.Translate, ]; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index f8320aa336..532effbd87 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.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/plugins/database/application/database_controller.dart'; @@ -9,15 +11,12 @@ import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.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'; -import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; - import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 6475535843..172ea91a8b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -9,7 +9,15 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:fixnum/fixnum.dart'; -class UserBackendService { +abstract class IUserBackendService { + Future> cancelSubscription(String workspaceId); + Future> createSubscription( + String workspaceId, + SubscriptionPlanPB plan, + ); +} + +class UserBackendService implements IUserBackendService { UserBackendService({required this.userId}); final Int64 userId; @@ -164,7 +172,7 @@ class UserBackendService { String workspaceId, ) async { final data = QueryWorkspacePB()..workspaceId = workspaceId; - return UserEventGetWorkspaceMember(data).send(); + return UserEventGetWorkspaceMembers(data).send(); } Future> addWorkspaceMember( @@ -225,20 +233,29 @@ class UserBackendService { return UserEventGetWorkspaceSubscriptions().send(); } - static Future> createSubscription( + Future> + getWorkspaceMember() async { + final data = WorkspaceMemberIdPB.create()..uid = userId; + + return UserEventGetMemberInfo(data).send(); + } + + @override + Future> createSubscription( String workspaceId, SubscriptionPlanPB plan, ) { final request = SubscribeWorkspacePB() ..workspaceId = workspaceId - ..recurringInterval = RecurringIntervalPB.Month + ..recurringInterval = RecurringIntervalPB.Year ..workspaceSubscriptionPlan = plan ..successUrl = '${getIt().appflowyCloudConfig.base_url}/web/payment-success'; return UserEventSubscribeWorkspace(request).send(); } - static Future> cancelSubscription( + @override + Future> cancelSubscription( String workspaceId, ) { final request = UserWorkspaceIdPB()..workspaceId = workspaceId; diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index a423217fae..8064f7038c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -156,7 +156,7 @@ class CommandPaletteBloc add(CommandPaletteEvent.performSearch(search: value)); void _onResultsChanged(RepeatedSearchResultPB results) => - add(CommandPaletteEvent.resultsChanged(results: results, max: 2)); + add(CommandPaletteEvent.resultsChanged(results: results)); } @freezed 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 d59b5b12c4..f09e08a3d1 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 @@ -118,6 +119,7 @@ class DesktopAppearance extends BaseAppearance { tint9: theme.tint9, textColor: theme.text, secondaryTextColor: theme.secondaryText, + strongText: theme.strongText, 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 09db07ed11..e2f1ee0006 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 @@ -250,6 +251,7 @@ class MobileAppearance extends BaseAppearance { tint9: theme.tint9, textColor: theme.text, secondaryTextColor: theme.secondaryText, + strongText: theme.strongText, greyHover: theme.hoverBG1, greySelect: theme.bg3, lightGreyHover: theme.hoverBG3, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart index 49b6cb246e..fd5587e322 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart @@ -13,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart'; import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'settings_plan_bloc.freezed.dart'; @@ -20,8 +21,10 @@ part 'settings_plan_bloc.freezed.dart'; class SettingsPlanBloc extends Bloc { SettingsPlanBloc({ required this.workspaceId, + required Int64 userId, }) : super(const _Initial()) { _service = WorkspaceService(workspaceId: workspaceId); + _userService = UserBackendService(userId: userId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); @@ -103,7 +106,7 @@ class SettingsPlanBloc extends Bloc { } }, addSubscription: (plan) async { - final result = await UserBackendService.createSubscription( + final result = await _userService.createSubscription( workspaceId, SubscriptionPlanPB.Pro, ); @@ -114,7 +117,7 @@ class SettingsPlanBloc extends Bloc { ); }, cancelSubscription: () async { - await UserBackendService.cancelSubscription(workspaceId); + await _userService.cancelSubscription(workspaceId); add(const SettingsPlanEvent.started()); }, paymentSuccessful: () { @@ -131,6 +134,7 @@ class SettingsPlanBloc extends Bloc { late final String workspaceId; late final WorkspaceService _service; + late final IUserBackendService _userService; late final SubscriptionSuccessListenable _successListenable; void _onPaymentSuccessful() { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart index fe6cb896bd..d6dde9e9c1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart @@ -13,4 +13,14 @@ extension SubscriptionLabels on WorkspaceSubscriptionPB { LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(), _ => 'N/A', }; + + String get info => switch (subscriptionPlan) { + SubscriptionPlanPB.None => + LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(), + SubscriptionPlanPB.Pro => + LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(), + SubscriptionPlanPB.Team => + LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(), + _ => 'N/A', + }; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index d7980e031a..3655f8a20d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -51,16 +51,10 @@ class WorkspaceSettingsBloc currentWorkspaceInList.workspaceId, ); - final role = members - .firstWhereOrNull((e) => e.email == userProfile.email) - ?.role ?? - AFRolePB.Guest; - emit( state.copyWith( workspace: currentWorkspaceInList, members: members, - myRole: role, ), ); } catch (e) { @@ -118,7 +112,7 @@ class WorkspaceSettingsBloc String workspaceId, ) async { final data = QueryWorkspacePB()..workspaceId = workspaceId; - final result = await UserEventGetWorkspaceMember(data).send(); + final result = await UserEventGetWorkspaceMembers(data).send(); return result.fold( (s) => s.items, (e) { @@ -150,7 +144,6 @@ class WorkspaceSettingsState with _$WorkspaceSettingsState { const factory WorkspaceSettingsState({ @Default(null) UserWorkspacePB? workspace, @Default([]) List members, - @Default(AFRolePB.Guest) AFRolePB myRole, @Default(false) bool deleteWorkspace, @Default(false) bool leaveWorkspace, }) = _WorkspaceSettingsState; diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 18a430ed04..2f79701707 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/user/application/user_listener.dart'; @@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -54,11 +55,21 @@ class UserWorkspaceBloc extends Bloc { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); await _userService.openWorkspace(currentWorkspace.workspaceId); } + + WorkspaceMemberPB? currentWorkspaceMember; + final workspaceMemberResult = + await _userService.getWorkspaceMember(); + currentWorkspaceMember = workspaceMemberResult.fold( + (s) => s, + (e) => null, + ); + emit( state.copyWith( currentWorkspace: currentWorkspace, workspaces: workspaces, isCollabWorkspaceOn: isCollabWorkspaceOn, + currentWorkspaceMember: currentWorkspaceMember, actionResult: null, ), ); @@ -198,6 +209,14 @@ class UserWorkspaceBloc extends Bloc { (e) => state.currentWorkspace, ); + WorkspaceMemberPB? currentWorkspaceMember; + final workspaceMemberResult = + await _userService.getWorkspaceMember(); + currentWorkspaceMember = workspaceMemberResult.fold( + (s) => s, + (e) => null, + ); + result ..onSuccess((s) { Log.info( @@ -211,6 +230,7 @@ class UserWorkspaceBloc extends Bloc { emit( state.copyWith( currentWorkspace: currentWorkspace, + currentWorkspaceMember: currentWorkspaceMember, actionResult: UserWorkspaceActionResult( actionType: UserWorkspaceActionType.open, isLoading: false, @@ -474,6 +494,7 @@ class UserWorkspaceState with _$UserWorkspaceState { const factory UserWorkspaceState({ @Default(null) UserWorkspacePB? currentWorkspace, @Default([]) List workspaces, + @Default(null) WorkspaceMemberPB? currentWorkspaceMember, @Default(null) UserWorkspaceActionResult? actionResult, @Default(false) bool isCollabWorkspaceOn, }) = _UserWorkspaceState; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index feffb61cc1..945976ef79 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.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/plugins/document/application/document_appearance_cubit.dart'; @@ -13,7 +15,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log; 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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -41,11 +42,30 @@ HotKeyItem openSettingsHotKey( }, ); -class UserSettingButton extends StatelessWidget { +class UserSettingButton extends StatefulWidget { const UserSettingButton({required this.userProfile, super.key}); final UserProfilePB userProfile; + @override + State createState() => _UserSettingButtonState(); +} + +class _UserSettingButtonState extends State { + late UserWorkspaceBloc _userWorkspaceBloc; + + @override + void initState() { + super.initState(); + _userWorkspaceBloc = context.read(); + } + + @override + void didChangeDependencies() { + _userWorkspaceBloc = context.read(); + super.didChangeDependencies(); + } + @override Widget build(BuildContext context) { return SizedBox.square( @@ -53,7 +73,11 @@ class UserSettingButton extends StatelessWidget { child: FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), child: FlowyButton( - onTap: () => showSettingsDialog(context, userProfile), + onTap: () => showSettingsDialog( + context, + widget.userProfile, + _userWorkspaceBloc, + ), margin: EdgeInsets.zero, text: const FlowySvg( FlowySvgs.settings_s, @@ -65,7 +89,11 @@ class UserSettingButton extends StatelessWidget { } } -void showSettingsDialog(BuildContext context, UserProfilePB userProfile) { +void showSettingsDialog( + BuildContext context, + UserProfilePB userProfile, [ + UserWorkspaceBloc? bloc, +]) { AFFocusManager.of(context).notifyLoseFocus(); showDialog( context: context, @@ -75,15 +103,10 @@ void showSettingsDialog(BuildContext context, UserProfilePB userProfile) { BlocProvider.value( value: BlocProvider.of(dialogContext), ), - BlocProvider.value(value: context.read()), + BlocProvider.value(value: bloc ?? context.read()), ], child: SettingsDialog( userProfile, - workspaceId: context - .read() - .state - .currentWorkspace! - .workspaceId, didLogout: () async { // Pop the dialog using the dialog context Navigator.of(dialogContext).pop(); 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 index d77c877601..e4df04647e 100644 --- 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 @@ -58,7 +58,6 @@ class _SettingsAccountViewState extends State { builder: (context, state) { return SettingsBody( title: LocaleKeys.settings_accountPage_title.tr(), - description: LocaleKeys.settings_accountPage_description.tr(), children: [ SettingsCategory( title: LocaleKeys.settings_accountPage_general_title.tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart index 53caaaee0e..d742451bb0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart @@ -10,15 +10,21 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_categor import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../generated/locale_keys.g.dart'; class SettingsBillingView extends StatelessWidget { - const SettingsBillingView({super.key, required this.workspaceId}); + const SettingsBillingView({ + super.key, + required this.workspaceId, + required this.user, + }); final String workspaceId; + final UserProfilePB user; @override Widget build(BuildContext context) { @@ -63,6 +69,7 @@ class SettingsBillingView extends StatelessWidget { onPressed: () => _openPricingDialog( context, workspaceId, + user.id, state.subscription, ), fontWeight: FontWeight.w500, @@ -116,13 +123,15 @@ class SettingsBillingView extends StatelessWidget { void _openPricingDialog( BuildContext context, String workspaceId, + Int64 userId, WorkspaceSubscriptionPB subscription, ) => showDialog( context: context, builder: (_) => BlocProvider( - create: (_) => SettingsPlanBloc(workspaceId: workspaceId) - ..add(const SettingsPlanEvent.started()), + create: (_) => + SettingsPlanBloc(workspaceId: workspaceId, userId: user.id) + ..add(const SettingsPlanEvent.started()), child: SettingsPlanComparisonDialog( workspaceId: workspaceId, subscription: subscription, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart index 05a6c5c033..f642a92b51 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart @@ -86,6 +86,7 @@ class _SettingsPlanComparisonDialogState child: FlowyDialog( constraints: const BoxConstraints(maxWidth: 784, minWidth: 674), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 24, left: 24, right: 24), @@ -120,8 +121,11 @@ class _SettingsPlanComparisonDialogState scrollDirection: Axis.horizontal, child: SingleChildScrollView( controller: verticalController, - padding: - const EdgeInsets.only(left: 24, right: 24, bottom: 24), + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -136,7 +140,7 @@ class _SettingsPlanComparisonDialogState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const VSpace(22), + const VSpace(26), SizedBox( height: 100, child: FlowyText.semibold( @@ -178,6 +182,7 @@ class _SettingsPlanComparisonDialogState canDowngrade: currentSubscription.subscriptionPlan != SubscriptionPlanPB.None, + currentCanceled: currentSubscription.hasCanceled, onSelected: () async { if (currentSubscription.subscriptionPlan == SubscriptionPlanPB.None || @@ -225,6 +230,7 @@ class _SettingsPlanComparisonDialogState SubscriptionPlanPB.Pro, canUpgrade: currentSubscription.subscriptionPlan == SubscriptionPlanPB.None, + currentCanceled: currentSubscription.hasCanceled, onSelected: () => context.read().add( const SettingsPlanEvent.addSubscription( @@ -257,6 +263,7 @@ class _PlanTable extends StatelessWidget { required this.onSelected, this.canUpgrade = false, this.canDowngrade = false, + this.currentCanceled = false, }); final String title; @@ -264,11 +271,12 @@ class _PlanTable extends StatelessWidget { final String price; final String priceInfo; - final List cells; + final List<_CellItem> cells; final bool isCurrent; final VoidCallback onSelected; final bool canUpgrade; final bool canDowngrade; + final bool currentCanceled; @override Widget build(BuildContext context) { @@ -291,8 +299,9 @@ class _PlanTable extends StatelessWidget { ? const EdgeInsets.only(top: 4) : const EdgeInsets.all(4), child: Container( - clipBehavior: Clip.antiAlias, - padding: const EdgeInsets.symmetric(vertical: 18), + padding: isCurrent + ? const EdgeInsets.only(bottom: 22) + : const EdgeInsets.symmetric(vertical: 22), decoration: BoxDecoration( borderRadius: BorderRadius.circular(22), color: Theme.of(context).cardColor, @@ -301,48 +310,55 @@ class _PlanTable extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (isCurrent) const _CurrentBadge(), _Heading( title: title, description: description, isPrimary: !highlightPlan, - horizontalInset: 12, ), _Heading( title: price, description: priceInfo, isPrimary: !highlightPlan, height: 64, - horizontalInset: 12, ), if (canUpgrade || canDowngrade) ...[ - Padding( - padding: const EdgeInsets.only(left: 12), - child: _ActionButton( - label: canUpgrade && !canDowngrade - ? LocaleKeys.settings_comparePlanDialog_actions_upgrade - .tr() - : LocaleKeys.settings_comparePlanDialog_actions_downgrade - .tr(), - onPressed: onSelected, - isUpgrade: canUpgrade && !canDowngrade, - useGradientBorder: !isCurrent && canUpgrade, - ), - ), - ] else if (isCurrent) ...[ - Padding( - padding: const EdgeInsets.only(left: 12), - child: _ActionButton( - label: LocaleKeys.settings_comparePlanDialog_actions_current - .tr(), - onPressed: () {}, - isUpgrade: canUpgrade && !canDowngrade, - useGradientBorder: !isCurrent && canUpgrade, + Opacity( + opacity: canDowngrade && currentCanceled ? 0.5 : 1, + child: Padding( + padding: EdgeInsets.only( + left: 12 + (canUpgrade && !canDowngrade ? 12 : 0), + ), + child: _ActionButton( + label: canUpgrade && !canDowngrade + ? LocaleKeys.settings_comparePlanDialog_actions_upgrade + .tr() + : LocaleKeys + .settings_comparePlanDialog_actions_downgrade + .tr(), + onPressed: !canUpgrade && canDowngrade && currentCanceled + ? null + : onSelected, + tooltip: !canUpgrade && canDowngrade && currentCanceled + ? LocaleKeys + .settings_comparePlanDialog_actions_downgradeDisabledTooltip + .tr() + : null, + isUpgrade: canUpgrade && !canDowngrade, + useGradientBorder: !isCurrent && canUpgrade, + ), ), ), ] else ...[ const SizedBox(height: 56), ], - ...cells.map((e) => _ComparisonCell(label: e)), + ...cells.map( + (cell) => _ComparisonCell( + label: cell.label, + icon: cell.icon, + isHighlighted: highlightPlan, + ), + ), ], ), ), @@ -350,16 +366,48 @@ class _PlanTable extends StatelessWidget { } } -class _ComparisonCell extends StatelessWidget { - const _ComparisonCell({required this.label, this.tooltip}); - - final String label; - final String? tooltip; +class _CurrentBadge extends StatelessWidget { + const _CurrentBadge(); @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 12), + margin: const EdgeInsets.only(left: 12), + height: 22, + width: 72, + decoration: BoxDecoration( + color: const Color(0xFF4F3F5F), + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: FlowyText.medium( + LocaleKeys.settings_comparePlanDialog_current.tr(), + fontSize: 12, + color: Colors.white, + ), + ), + ); + } +} + +class _ComparisonCell extends StatelessWidget { + const _ComparisonCell({ + required this.label, + this.icon, + this.tooltip, + this.isHighlighted = false, + }); + + final String label; + final FlowySvgData? icon; + final String? tooltip; + final bool isHighlighted; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12) + + EdgeInsets.only(left: isHighlighted ? 12 : 0), height: 36, decoration: BoxDecoration( border: Border( @@ -371,7 +419,16 @@ class _ComparisonCell extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Expanded(child: FlowyText.medium(label)), + if (icon != null) ...[ + FlowySvg(icon!), + ] else ...[ + Expanded( + child: FlowyText.medium( + label, + lineHeight: 1.2, + ), + ), + ], if (tooltip != null) FlowyTooltip( message: tooltip, @@ -386,13 +443,15 @@ class _ComparisonCell extends StatelessWidget { class _ActionButton extends StatelessWidget { const _ActionButton({ required this.label, + this.tooltip, required this.onPressed, required this.isUpgrade, this.useGradientBorder = false, }); final String label; - final VoidCallback onPressed; + final String? tooltip; + final VoidCallback? onPressed; final bool isUpgrade; final bool useGradientBorder; @@ -405,31 +464,36 @@ class _ActionButton extends StatelessWidget { height: 56, child: Row( children: [ - GestureDetector( - onTap: onPressed, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: _drawGradientBorder( - isLM: isLM, - child: Container( - height: gradientBorder ? 36 : 40, - width: gradientBorder ? 148 : 152, - decoration: BoxDecoration( - color: gradientBorder - ? Theme.of(context).cardColor - : Colors.transparent, - border: Border.all( - color: gradientBorder - ? Colors.transparent - : AFThemeExtension.of(context).textColor, + FlowyTooltip( + message: tooltip, + child: GestureDetector( + onTap: onPressed, + child: MouseRegion( + cursor: onPressed != null + ? SystemMouseCursors.click + : MouseCursor.defer, + child: _drawGradientBorder( + isLM: isLM, + child: Container( + height: gradientBorder ? 36 : 40, + width: gradientBorder ? 148 : 152, + decoration: BoxDecoration( + color: useGradientBorder + ? Theme.of(context).cardColor + : Colors.transparent, + border: Border.all( + color: gradientBorder + ? Colors.transparent + : AFThemeExtension.of(context).textColor, + ), + borderRadius: + BorderRadius.circular(gradientBorder ? 14 : 16), ), - borderRadius: - BorderRadius.circular(gradientBorder ? 14 : 16), - ), - child: Center( - child: _drawText( - label, - isLM, + child: Center( + child: _drawText( + label, + isLM, + ), ), ), ), @@ -445,6 +509,7 @@ class _ActionButton extends StatelessWidget { final child = FlowyText( text, fontSize: 14, + lineHeight: 1.2, fontWeight: useGradientBorder ? FontWeight.w600 : FontWeight.w500, ); @@ -495,29 +560,29 @@ class _Heading extends StatelessWidget { this.description, this.isPrimary = true, this.height = 100, - this.horizontalInset = 0, }); final String title; final String? description; final bool isPrimary; final double height; - final double horizontalInset; @override Widget build(BuildContext context) { return SizedBox( - width: 165, + width: 175, height: height, child: Padding( - padding: EdgeInsets.only(left: horizontalInset), + padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FlowyText.semibold( title, fontSize: 24, - color: isPrimary ? null : const Color(0xFF5C3699), + color: isPrimary + ? AFThemeExtension.of(context).strongText + : const Color(0xFF5C3699), ), if (description != null && description!.isNotEmpty) ...[ const VSpace(4), @@ -525,6 +590,7 @@ class _Heading extends StatelessWidget { description!, fontSize: 12, maxLines: 3, + lineHeight: 1.5, ), ], ], @@ -571,24 +637,67 @@ final _planLabels = [ ), ]; -final _freeLabels = [ - LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(), - LocaleKeys.settings_comparePlanDialog_freeLabels_itemEight.tr(), +class _CellItem { + const _CellItem(this.label, {this.icon}); + + final String label; + final FlowySvgData? icon; +} + +final List<_CellItem> _freeLabels = [ + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(), + icon: FlowySvgs.check_m, + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(), + icon: FlowySvgs.check_m, + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemEight.tr(), + ), ]; -final _proLabels = [ - LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(), - LocaleKeys.settings_comparePlanDialog_proLabels_itemEight.tr(), +final List<_CellItem> _proLabels = [ + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(), + icon: FlowySvgs.check_m, + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(), + icon: FlowySvgs.check_m, + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemEight.tr(), + ), ]; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 091730ab81..9aa84d2e67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -13,6 +13,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/flowy_gradient_b import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -21,15 +22,22 @@ import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsPlanView extends StatelessWidget { - const SettingsPlanView({super.key, required this.workspaceId}); + const SettingsPlanView({ + super.key, + required this.workspaceId, + required this.user, + }); final String workspaceId; + final UserProfilePB user; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => SettingsPlanBloc(workspaceId: workspaceId) - ..add(const SettingsPlanEvent.started()), + create: (context) => SettingsPlanBloc( + workspaceId: workspaceId, + userId: user.id, + )..add(const SettingsPlanEvent.started()), child: BlocBuilder( builder: (context, state) { return state.map( @@ -97,16 +105,17 @@ class _CurrentPlanBox extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const VSpace(4), FlowyText.semibold( subscription.label, fontSize: 24, + color: AFThemeExtension.of(context).strongText, ), - const VSpace(4), + const VSpace(8), FlowyText.regular( - LocaleKeys - .settings_planPage_planUsage_currentPlan_freeInfo - .tr(), + subscription.info, fontSize: 16, + color: AFThemeExtension.of(context).strongText, maxLines: 3, ), const VSpace(16), @@ -262,7 +271,7 @@ class _ProConItem extends StatelessWidget { height: 24, width: 24, child: FlowySvg( - isPro ? FlowySvgs.check_s : FlowySvgs.close_s, + isPro ? FlowySvgs.check_m : FlowySvgs.close_s, color: isPro ? null : const Color(0xFF900000), ), ), @@ -271,7 +280,8 @@ class _ProConItem extends StatelessWidget { child: FlowyText.regular( label, fontSize: 12, - maxLines: 2, + color: AFThemeExtension.of(context).strongText, + maxLines: 3, ), ), ], @@ -372,7 +382,7 @@ class _UsageBox extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowyText.regular( + FlowyText.medium( title, fontSize: 11, color: AFThemeExtension.of(context).secondaryTextColor, @@ -443,7 +453,11 @@ class _ToggleMoreState extends State<_ToggleMore> { }, ), const HSpace(10), - FlowyText.regular(widget.label, fontSize: 14), + FlowyText.regular( + widget.label, + fontSize: 14, + color: AFThemeExtension.of(context).strongText, + ), if (widget.badgeLabel != null && widget.badgeLabel!.isNotEmpty) ...[ const HSpace(10), SizedBox( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 4432618eaa..74ccac5558 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,5 +1,6 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -44,14 +45,18 @@ import 'package:flowy_infra/theme_extension.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/dialog/styled_dialogs.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; class SettingsWorkspaceView extends StatelessWidget { - const SettingsWorkspaceView({super.key, required this.userProfile}); + const SettingsWorkspaceView({ + super.key, + required this.userProfile, + this.workspaceMember, + }); final UserProfilePB userProfile; + final WorkspaceMemberPB? workspaceMember; @override Widget build(BuildContext context) { @@ -87,7 +92,7 @@ class SettingsWorkspaceView extends StatelessWidget { SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), - children: const [_WorkspaceNameSetting()], + children: [_WorkspaceNameSetting(member: workspaceMember)], ), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceIcon_title @@ -97,7 +102,7 @@ class SettingsWorkspaceView extends StatelessWidget { .tr(), children: [ _WorkspaceIconSetting( - enableEdit: state.myRole.isOwner, + enableEdit: workspaceMember?.role.isOwner ?? false, workspace: state.workspace, ), ], @@ -113,7 +118,6 @@ class SettingsWorkspaceView extends StatelessWidget { LocaleKeys.settings_workspacePage_theme_description.tr(), children: const [ _ThemeDropdown(), - SettingsDashedDivider(), _DocumentCursorColorSetting(), _DocumentSelectionColorSetting(), ], @@ -121,14 +125,20 @@ class SettingsWorkspaceView extends StatelessWidget { SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceFont_title.tr(), - children: const [_FontSelectorDropdown()], - ), - SettingsCategory( - title: - LocaleKeys.settings_workspacePage_textDirection_title.tr(), - children: const [ - TextDirectionSelect(), - EnableRTLItemsSwitcher(), + children: [ + _FontSelectorDropdown( + currentFont: + context.read().state.font, + ), + const SettingsDashedDivider(), + SettingsCategory( + title: LocaleKeys.settings_workspacePage_textDirection_title + .tr(), + children: const [ + TextDirectionSelect(), + EnableRTLItemsSwitcher(), + ], + ), ], ), SettingsCategory( @@ -158,14 +168,14 @@ class SettingsWorkspaceView extends StatelessWidget { fontSize: 16, fontWeight: FontWeight.w600, onPressed: () => SettingsAlertDialog( - title: state.myRole.isOwner + title: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_deleteWorkspacePrompt_title .tr() : LocaleKeys .settings_workspacePage_leaveWorkspacePrompt_title .tr(), - subtitle: state.myRole.isOwner + subtitle: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_deleteWorkspacePrompt_content .tr() @@ -175,7 +185,7 @@ class SettingsWorkspaceView extends StatelessWidget { isDangerous: true, confirm: () { context.read().add( - state.myRole.isOwner + workspaceMember?.role.isOwner ?? false ? const WorkspaceSettingsEvent.deleteWorkspace() : const WorkspaceSettingsEvent.leaveWorkspace(), ); @@ -183,7 +193,7 @@ class SettingsWorkspaceView extends StatelessWidget { }, ).show(context), isDangerous: true, - buttonLabel: state.myRole.isOwner + buttonLabel: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_manageWorkspace_deleteWorkspace .tr() @@ -201,7 +211,9 @@ class SettingsWorkspaceView extends StatelessWidget { } class _WorkspaceNameSetting extends StatefulWidget { - const _WorkspaceNameSetting(); + const _WorkspaceNameSetting({this.member}); + + final WorkspaceMemberPB? member; @override State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); @@ -209,31 +221,8 @@ class _WorkspaceNameSetting extends StatefulWidget { class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { final TextEditingController workspaceNameController = TextEditingController(); - late final FocusNode focusNode; - bool isEditing = false; - - @override - void initState() { - super.initState(); - focusNode = FocusNode( - onKeyEvent: (_, event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape && - isEditing && - mounted) { - setState(() => isEditing = false); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - )..addListener(() { - if (!focusNode.hasFocus && isEditing && mounted) { - _saveWorkspaceName(name: workspaceNameController.text); - setState(() => isEditing = false); - } - }); - } + final focusNode = FocusNode(); + Timer? _debounce; @override void dispose() { @@ -252,69 +241,44 @@ class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { } }, builder: (_, state) { - if (isEditing) { - return Flexible( - child: SettingsInputField( - textController: workspaceNameController, - value: workspaceNameController.text, - focusNode: focusNode..requestFocus(), - onCancel: () => setState(() => isEditing = false), - onSave: (_) { - _saveWorkspaceName(name: workspaceNameController.text); - setState(() => isEditing = false); - }, + if (widget.member == null || !widget.member!.role.isOwner) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.5), + child: FlowyText.regular( + workspaceNameController.text, + fontSize: 14, ), ); } - return Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 2.5), - child: FlowyText.regular( - workspaceNameController.text, - fontSize: 14, - ), - ), - if (state.myRole.isOwner) ...[ - const HSpace(4), - FlowyTooltip( - message: LocaleKeys - .settings_workspacePage_workspaceName_editTooltip - .tr(), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => isEditing = true), - child: const FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: EdgeInsets.all(4), - child: FlowySvg(FlowySvgs.edit_s), - ), - ), - ), - ), - ], - ], + return Flexible( + child: SettingsInputField( + textController: workspaceNameController, + value: workspaceNameController.text, + focusNode: focusNode, + onSave: (_) => + _saveWorkspaceName(name: workspaceNameController.text), + onChanged: _debounceSaveName, + hideActions: true, + ), ); }, ); } - void _saveWorkspaceName({ - required String name, - }) { - if (name.isNotEmpty) { - context.read().add( - WorkspaceSettingsEvent.updateWorkspaceName(name), - ); + void _debounceSaveName(String name) { + _debounce?.cancel(); + _debounce = Timer( + const Duration(milliseconds: 300), + () => _saveWorkspaceName(name: name), + ); + } - if (context.mounted) { - showSnackBarMessage( - context, - LocaleKeys.settings_workspacePage_workspaceName_savedMessage.tr(), - ); - } + void _saveWorkspaceName({required String name}) { + if (name.isNotEmpty) { + context + .read() + .add(WorkspaceSettingsEvent.updateWorkspaceName(name)); } } } @@ -636,7 +600,9 @@ class _ThemeDropdown extends StatelessWidget { key: const Key('ThemeSelectorDropdown'), actions: [ SettingAction( - tooltip: 'Upload a custom theme', + tooltip: LocaleKeys + .settings_workspacePage_theme_uploadCustomThemeTooltip + .tr(), icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)), onPressed: () => Dialogs.show( context, @@ -843,7 +809,9 @@ class _SelectedModeIndicator extends StatelessWidget { } class _FontSelectorDropdown extends StatefulWidget { - const _FontSelectorDropdown(); + const _FontSelectorDropdown({required this.currentFont}); + + final String currentFont; @override State<_FontSelectorDropdown> createState() => _FontSelectorDropdownState(); @@ -853,18 +821,23 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { late final _options = [defaultFontFamily, ...GoogleFonts.asMap().keys]; final _focusNode = FocusNode(); final _controller = PopoverController(); - final _scrollController = ScrollController(); + late final ScrollController _scrollController; + final _textController = TextEditingController(); + + @override + void initState() { + super.initState(); + const itemExtent = 32; + final index = _options.indexOf(widget.currentFont); + final newPosition = (index * itemExtent).toDouble(); + _scrollController = ScrollController(initialScrollOffset: newPosition); - void _scrollIfNeccessary() { WidgetsBinding.instance.addPostFrameCallback((_) { - // Set scroll position to selected item. - final appearance = context.read().state; - const itemExtent = 32; - final index = _options.indexOf(appearance.font); - final newPosition = (index * itemExtent).toDouble(); - if (_scrollController.offset != newPosition) { - _scrollController.jumpTo(newPosition); - } + _textController.text = context + .read() + .state + .font + .fontFamilyDisplayName; }); } @@ -873,6 +846,7 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { _controller.close(); _focusNode.dispose(); _scrollController.dispose(); + _textController.dispose(); super.dispose(); } @@ -904,57 +878,14 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { ), ], ), - popupBuilder: (_) { - _scrollIfNeccessary(); - return Material( - type: MaterialType.transparency, - child: ListView.separated( - controller: _scrollController, - padding: const EdgeInsets.symmetric(horizontal: 6), - itemCount: _options.length, - separatorBuilder: (_, __) => const VSpace(4), - itemBuilder: (context, index) { - final font = _options[index]; - final isSelected = appearance.font == font; - return SizedBox( - height: 28, - child: ListTile( - selected: isSelected, - dense: true, - hoverColor: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.12), - selectedTileColor: - Theme.of(context).colorScheme.primary.withOpacity(0.12), - contentPadding: const EdgeInsets.symmetric(horizontal: 6), - minTileHeight: 28, - onTap: () { - context - .read() - .setFontFamily(font); - - // This is a workaround such that when dialog rebuilds due - // to font changing, the font selector won't retain focus. - _focusNode.parent?.requestFocus(); - - _controller.close(); - }, - title: Text( - font.fontFamilyDisplayName, - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontFamily: getGoogleFontSafely(font).fontFamily, - ), - ), - trailing: - isSelected ? const FlowySvg(FlowySvgs.check_s) : null, - ), - ); - }, - ), - ); - }, + popupBuilder: (_) => _FontListPopup( + currentFont: appearance.font, + scrollController: _scrollController, + controller: _controller, + options: _options, + textController: _textController, + focusNode: _focusNode, + ), child: Row( children: [ Expanded( @@ -970,36 +901,43 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { setState(() {}); _controller.show(); }, - child: Focus( + child: FlowyTextField( + autoFocus: false, focusNode: _focusNode, - includeSemantics: false, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - border: Border.all( - color: _focusNode.hasFocus - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, + controller: _textController, + decoration: InputDecoration( + suffixIcon: const MouseRegion( + cursor: SystemMouseCursors.click, + child: Icon(Icons.arrow_drop_down), + ), + counterText: '', + contentPadding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 18, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, ), borderRadius: Corners.s8Border, ), - child: Row( - children: [ - const HSpace(18), - Text( - appearance.font.fontFamilyDisplayName, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(fontFamily: appearance.font), - ), - const Spacer(), - const MouseRegion( - cursor: SystemMouseCursors.click, - child: Icon(Icons.arrow_drop_down), - ), - const HSpace(10), - ], + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: Corners.s8Border, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s8Border, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s8Border, ), ), ), @@ -1042,6 +980,145 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { } } +class _FontListPopup extends StatefulWidget { + const _FontListPopup({ + required this.controller, + required this.scrollController, + required this.options, + required this.currentFont, + required this.textController, + required this.focusNode, + }); + + final ScrollController scrollController; + final List options; + final String currentFont; + final TextEditingController textController; + final FocusNode focusNode; + final PopoverController controller; + + @override + State<_FontListPopup> createState() => _FontListPopupState(); +} + +class _FontListPopupState extends State<_FontListPopup> { + late List _filteredOptions = widget.options; + + @override + void initState() { + super.initState(); + widget.textController.addListener(_onTextFieldChanged); + } + + void _onTextFieldChanged() { + final value = widget.textController.text; + + if (value.trim().isEmpty) { + _filteredOptions = widget.options; + } else { + if (value.fontFamilyDisplayName == + widget.currentFont.fontFamilyDisplayName) { + return; + } + + _filteredOptions = widget.options + .where( + (f) => + f.toLowerCase().contains(value.trim().toLowerCase()) || + f.fontFamilyDisplayName + .toLowerCase() + .contains(value.trim().fontFamilyDisplayName.toLowerCase()), + ) + .toList(); + + // Default font family is "", but the display name is "System", + // which means it's hard compared to other font families to find this one. + if (!_filteredOptions.contains(defaultFontFamily) && + 'system'.contains(value.trim().toLowerCase())) { + _filteredOptions.insert(0, defaultFontFamily); + } + } + + setState(() {}); + } + + @override + void dispose() { + widget.textController.removeListener(_onTextFieldChanged); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_filteredOptions.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: FlowyText.medium( + LocaleKeys.settings_workspacePage_workspaceFont_noFontHint.tr(), + ), + ), + Flexible( + child: ListView.separated( + shrinkWrap: _filteredOptions.length < 10, + controller: widget.scrollController, + padding: const EdgeInsets.symmetric(horizontal: 6), + itemCount: _filteredOptions.length, + separatorBuilder: (_, __) => const VSpace(4), + itemBuilder: (context, index) { + final font = _filteredOptions[index]; + final isSelected = widget.currentFont == font; + return SizedBox( + height: 28, + child: ListTile( + selected: isSelected, + dense: true, + hoverColor: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.12), + selectedTileColor: + Theme.of(context).colorScheme.primary.withOpacity(0.12), + contentPadding: const EdgeInsets.symmetric(horizontal: 6), + minTileHeight: 28, + onTap: () { + context + .read() + .setFontFamily(font); + + widget.textController.text = font.fontFamilyDisplayName; + + // This is a workaround such that when dialog rebuilds due + // to font changing, the font selector won't retain focus. + widget.focusNode.parent?.requestFocus(); + + widget.controller.close(); + }, + title: Text( + font.fontFamilyDisplayName, + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontFamily: getGoogleFontSafely(font).fontFamily, + ), + ), + trailing: + isSelected ? const FlowySvg(FlowySvgs.check_s) : null, + ), + ); + }, + ), + ), + ], + ), + ); + } +} + class _DocumentCursorColorSetting extends StatelessWidget { const _DocumentCursorColorSetting(); 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 4ee2ad9203..09aeeacba5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -2,6 +2,7 @@ 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/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; @@ -13,6 +14,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_custom import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,14 +26,12 @@ class SettingsDialog extends StatelessWidget { required this.dismissDialog, required this.didLogout, required this.restartApp, - required this.workspaceId, }) : super(key: ValueKey(user.id)); final VoidCallback dismissDialog; final VoidCallback didLogout; final VoidCallback restartApp; final UserProfilePB user; - final String workspaceId; @override Widget build(BuildContext context) { @@ -57,12 +57,25 @@ class SettingsDialog extends StatelessWidget { .add(SettingsDialogEvent.setSelectedPage(index)), currentPage: context.read().state.page, + member: context + .read() + .state + .currentWorkspaceMember, ), ), Expanded( child: getSettingsView( + context + .read() + .state + .currentWorkspace! + .workspaceId, context.read().state.page, context.read().state.userProfile, + context + .read() + .state + .currentWorkspaceMember, ), ), ], @@ -74,7 +87,12 @@ class SettingsDialog extends StatelessWidget { ); } - Widget getSettingsView(SettingsPage page, UserProfilePB user) { + Widget getSettingsView( + String workspaceId, + SettingsPage page, + UserProfilePB user, + WorkspaceMemberPB? member, + ) { switch (page) { case SettingsPage.account: return SettingsAccountView( @@ -83,7 +101,10 @@ class SettingsDialog extends StatelessWidget { didLogin: dismissDialog, ); case SettingsPage.workspace: - return SettingsWorkspaceView(userProfile: user); + return SettingsWorkspaceView( + userProfile: user, + workspaceMember: member, + ); case SettingsPage.manageData: return SettingsManageDataView(userProfile: user); case SettingsPage.notifications: @@ -95,9 +116,9 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.member: return WorkspaceMembersPage(userProfile: user); case SettingsPage.plan: - return SettingsPlanView(workspaceId: workspaceId); + return SettingsPlanView(workspaceId: workspaceId, user: user); case SettingsPage.billing: - return SettingsBillingView(workspaceId: workspaceId); + return SettingsBillingView(workspaceId: workspaceId, user: user); case SettingsPage.featureFlags: return const FeatureFlagsPage(); default: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart index 41e8733cc5..2d56c9dce8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart @@ -71,7 +71,7 @@ class _FlowyGradientButtonState extends State { ), ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 8), child: FlowyText( widget.label, fontSize: 16, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 42f407f97a..91d8e1b3e7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/flutter/af_dropdown_menu.dart'; +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -53,10 +53,10 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, - textStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(fontFamily: fontFamilyUsed), + textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontFamily: fontFamilyUsed, + fontWeight: FontWeight.w400, + ), menuStyle: MenuStyle( maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 250)), 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 index 4ca3f8f508..a41e98dfc8 100644 --- 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 @@ -27,6 +27,7 @@ class SettingsInputField extends StatefulWidget { this.onSave, this.onCancel, this.hideActions = false, + this.onChanged, }); final String? label; @@ -47,14 +48,16 @@ class SettingsInputField extends StatefulWidget { /// final bool hideActions; - final Function(String)? onSave; + final void 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; + final void Function()? onCancel; + + final void Function(String)? onChanged; @override State createState() => _SettingsInputFieldState(); @@ -127,7 +130,10 @@ class _SettingsInputFieldState extends State { ), ), onSubmitted: widget.onSave, - onChanged: (_) => setState(() {}), + onChanged: (_) { + widget.onChanged?.call(controller.text); + setState(() {}); + }, ), ), if (!widget.hideActions && 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 6b9e55fc7d..2d2f2e57b7 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 @@ -143,6 +143,8 @@ class _InviteMemberState extends State<_InviteMember> { height: 48.0, ), child: FlowyTextField( + hintText: + LocaleKeys.settings_appearance_members_inviteHint.tr(), controller: _emailController, onEditingComplete: _inviteMember, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index a75cd87171..1d8795411d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -9,16 +9,18 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_b import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.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/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flowy_infra/theme_extension.dart'; class AppFlowyCloudViewSetting extends StatelessWidget { const AppFlowyCloudViewSetting({ @@ -43,11 +45,11 @@ class AppFlowyCloudViewSetting extends StatelessWidget { (setting) => _renderContent(context, setting), (err) => FlowyErrorPage.message(err.toString(), howToFix: ""), ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); } + + return const Center( + child: CircularProgressIndicator(), + ); }, ); } @@ -314,14 +316,12 @@ class AppFlowyCloudEnableSync extends StatelessWidget { children: [ FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), const Spacer(), - Switch.adaptive( - onChanged: (bool value) { - context.read().add( - AppFlowyCloudSettingEvent.enableSync(value), - ); - }, - activeColor: Theme.of(context).colorScheme.primary, + Toggle( + style: ToggleStyle.big, value: state.setting.enableSync, + onChanged: (value) => context + .read() + .add(AppFlowyCloudSettingEvent.enableSync(!value)), ), ], ); 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 e86706a999..393fd92f0a 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 @@ -3,6 +3,7 @@ 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/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'; @@ -16,11 +17,13 @@ class SettingsMenu extends StatelessWidget { required this.changeSelectedPage, required this.currentPage, required this.userProfile, + this.member, }); final Function changeSelectedPage; final SettingsPage currentPage; final UserProfilePB userProfile; + final WorkspaceMemberPB? member; @override Widget build(BuildContext context) { @@ -99,7 +102,11 @@ class SettingsMenu extends StatelessWidget { icon: const Icon(Icons.cut), changeSelectedPage: changeSelectedPage, ), - if (FeatureFlag.planBilling.isOn) ...[ + if (FeatureFlag.planBilling.isOn && + userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud && + member != null && + member!.role.isOwner) ...[ SettingsMenuElement( page: SettingsPage.plan, selectedPage: currentPage, 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 c940f68451..0c672a1f85 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -72,6 +72,7 @@ class FlowyColorScheme { required this.icon, required this.text, required this.secondaryText, + required this.strongText, required this.input, required this.hint, required this.primary, @@ -123,6 +124,7 @@ class FlowyColorScheme { final Color icon; final Color text; final Color secondaryText; + final Color strongText; final Color input; final Color hint; final Color primary; 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 7c3939e5fe..9ca6bf1045 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -64,6 +64,7 @@ class DandelionColorScheme extends FlowyColorScheme { icon: _lightShader1, text: _lightShader1, secondaryText: _lightShader1, + strongText: Colors.black, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -119,6 +120,7 @@ class DandelionColorScheme extends FlowyColorScheme { icon: _darkShader5, text: _darkShader5, secondaryText: _darkShader5, + strongText: Colors.white, 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 ab83df5496..e87fbbc424 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 @@ -60,6 +60,7 @@ class DefaultColorScheme extends FlowyColorScheme { icon: _lightShader1, text: _lightShader1, secondaryText: const Color(0xFF4f4f4f), + strongText: Colors.black, input: _white, hint: _lightShader3, primary: _lightMain1, @@ -113,6 +114,7 @@ class DefaultColorScheme extends FlowyColorScheme { icon: _darkShader5, text: _darkShader5, secondaryText: _darkShader5, + strongText: Colors.white, input: _darkInput, hint: const Color(0xFF59647a), primary: _darkMain2, 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 87fea48cdd..01eb66b02d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -62,6 +62,7 @@ class LavenderColorScheme extends FlowyColorScheme { icon: _lightShader1, text: _lightShader1, secondaryText: _lightShader1, + strongText: Colors.black, input: _white, hint: _lightShader3, primary: _lightMain1, @@ -115,6 +116,7 @@ class LavenderColorScheme extends FlowyColorScheme { icon: _darkShader5, text: _darkShader5, secondaryText: _darkShader5, + strongText: Colors.white, 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 d2a80f7b92..115de1e442 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -66,6 +66,7 @@ class LemonadeColorScheme extends FlowyColorScheme { icon: _lightShader1, text: _lightShader1, secondaryText: _lightShader1, + strongText: Colors.black, input: _white, hint: _lightShader3, primary: _lightDandelionYellow, @@ -121,6 +122,7 @@ class LemonadeColorScheme extends FlowyColorScheme { icon: _darkShader5, text: _darkShader5, secondaryText: _darkShader5, + strongText: Colors.white, 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 70128c44bd..48b4dfe86d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -26,6 +26,7 @@ class AFThemeExtension extends ThemeExtension { required this.toggleOffFill, required this.textColor, required this.secondaryTextColor, + required this.strongText, required this.calloutBGColor, required this.tableCellBGColor, required this.calendarWeekendBGColor, @@ -54,6 +55,7 @@ class AFThemeExtension extends ThemeExtension { final Color textColor; final Color secondaryTextColor; + final Color strongText; final Color greyHover; final Color greySelect; final Color lightGreyHover; @@ -87,6 +89,7 @@ class AFThemeExtension extends ThemeExtension { Color? tint9, Color? textColor, Color? secondaryTextColor, + Color? strongText, Color? calloutBGColor, Color? tableCellBGColor, Color? greyHover, @@ -117,6 +120,7 @@ class AFThemeExtension extends ThemeExtension { tint9: tint9 ?? this.tint9, textColor: textColor ?? this.textColor, secondaryTextColor: secondaryTextColor ?? this.secondaryTextColor, + strongText: strongText ?? this.strongText, calloutBGColor: calloutBGColor ?? this.calloutBGColor, tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor, greyHover: greyHover ?? this.greyHover, @@ -159,6 +163,11 @@ class AFThemeExtension extends ThemeExtension { other.secondaryTextColor, t, )!, + strongText: Color.lerp( + strongText, + other.strongText, + t, + )!, calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!, tableCellBGColor: Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index b5c164bda7..a4768969af 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; + import 'package:google_fonts/google_fonts.dart'; class FlowyText extends StatelessWidget { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index a50d500348..1eb8bd0046 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -22,6 +22,10 @@ class FlowyTooltip extends StatelessWidget { @override Widget build(BuildContext context) { + if (message == null && richMessage == null) { + return child ?? const SizedBox.shrink(); + } + final isLightMode = Theme.of(context).brightness == Brightness.light; return Tooltip( margin: margin, diff --git a/frontend/resources/flowy_icons/24x/check.svg b/frontend/resources/flowy_icons/24x/check.svg new file mode 100644 index 0000000000..9c158465f7 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/check.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 50a3395568..842b447a98 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -359,7 +359,6 @@ "accountPage": { "menuLabel": "My account", "title": "My account", - "description": "Customize your profile, manage account security and AI API keys, or login into your account.", "general": { "title": "Account name & profile image", "changeProfilePicture": "Change profile picture" @@ -390,9 +389,7 @@ "title": "Workspace", "description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.", "workspaceName": { - "title": "Workspace name", - "savedMessage": "Saved workspace name", - "editTooltip": "Edit workspace name" + "title": "Workspace name" }, "workspaceIcon": { "title": "Workspace icon", @@ -409,10 +406,12 @@ }, "theme": { "title": "Theme", - "description": "Select a preset theme, or upload your own custom theme." + "description": "Select a preset theme, or upload your own custom theme.", + "uploadCustomThemeTooltip": "Upload a custom theme" }, "workspaceFont": { - "title": "Workspace font" + "title": "Workspace font", + "noFontHint": "No font found, try another term." }, "textDirection": { "title": "Text direction", @@ -531,7 +530,9 @@ "freeTitle": "Free", "proTitle": "Pro", "teamTitle": "Team", - "freeInfo": "Perfect for individuals or small teams up to 3.", + "freeInfo": "Perfect for individuals or small teams up to 3 members.", + "proInfo": "Perfect for small and medium teams up to 10 members.", + "teamInfo": "Perfect for all productive and well-organized teams..", "upgrade": "Compare &\n Upgrade", "freeProOne": "Collaborative workspace", "freeProTwo": "Up to 3 members (incl. owner)", @@ -579,9 +580,11 @@ "comparePlanDialog": { "title": "Compare & select plan", "planFeatures": "Plan\nFeatures", + "current": "Current", "actions": { "upgrade": "Upgrade", "downgrade": "Downgrade", + "downgradeDisabledTooltip": "You will automatically downgrade at the end of the billing cycle", "current": "Current" }, "freePlan": { @@ -592,7 +595,7 @@ }, "proPlan": { "title": "Professional", - "description": "A palce for small groups to plan & get organized.", + "description": "A place for small groups to plan & get organized.", "price": "$10 /month", "priceInfo": "billed annually" }, @@ -677,7 +680,7 @@ "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Input the websocket address of your server", "restartApp": "Restart", - "restartAppTip": "Restart the application for the changes to take effect. Please note that this might log out your current account", + "restartAppTip": "Restart the application for the changes to take effect. Please note that this might log out your current account.", "changeServerTip": "After changing the server, you must click the restart button for the changes to take effect", "enableEncryptPrompt": "Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy", "inputEncryptPrompt": "Please enter your encryption secret for", @@ -775,10 +778,11 @@ "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page", "enableRTLToolbarItems": "Enable RTL toolbar items", "members": { - "title": "Members Settings", - "inviteMembers": "Invite Members", - "sendInvite": "Send Invite", - "copyInviteLink": "Copy Invite Link", + "title": "Members settings", + "inviteMembers": "Invite members", + "inviteHint": "Invite by email", + "sendInvite": "Send invite", + "copyInviteLink": "Copy invite link", "label": "Members", "user": "User", "role": "Role", @@ -786,7 +790,7 @@ "owner": "Owner", "guest": "Guest", "member": "Member", - "memberHintText": "A member can read, comment, and edit pages. Invite members and guests.", + "memberHintText": "A member can read and edit pages", "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", "emailInvalidError": "Invalid email, please check and try again", "emailSent": "Email sent, please check the inbox", diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 194d15a54c..af4de3f5d0 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -85,7 +85,7 @@ impl EventIntegrationTest { pub async fn get_workspace_members(&self, workspace_id: &str) -> Vec { EventBuilder::new(self.clone()) - .event(UserEvent::GetWorkspaceMember) + .event(UserEvent::GetWorkspaceMembers) .payload(QueryWorkspacePB { workspace_id: workspace_id.to_string(), }) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs index b31853a803..cbb6e3c7d7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs @@ -1,5 +1,4 @@ use flowy_folder::manager::FolderManager; -use flowy_search::document::handler::DocumentSearchHandler; use flowy_search::folder::handler::FolderSearchHandler; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; @@ -10,11 +9,12 @@ pub struct SearchDepsResolver(); impl SearchDepsResolver { pub async fn resolve( folder_indexer: Arc, - cloud_service: Arc, - folder_manager: Arc, + _cloud_service: Arc, + _folder_manager: Arc, ) -> Arc { let folder_handler = Arc::new(FolderSearchHandler::new(folder_indexer)); - let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager)); - Arc::new(SearchManager::new(vec![folder_handler, document_handler])) + // TODO(Mathias): Enable when Cloud Search is ready + // let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager)); + Arc::new(SearchManager::new(vec![folder_handler])) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index dc82ad3e17..a7928a9747 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -339,6 +339,23 @@ where }) } + fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> FutureResult { + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let query = QueryWorkspaceMember { + workspace_id: workspace_id.clone(), + uid, + }; + let member = client.get_workspace_member(query).await?; + Ok(from_af_workspace_member(member)) + }) + } + #[instrument(level = "debug", skip_all)] fn get_user_awareness_doc_state( &self, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index ccfc3f88b0..a97f7b28e1 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -231,6 +231,14 @@ pub trait UserCloudService: Send + Sync + 'static { FutureResult::new(async { Ok(vec![]) }) } + fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> FutureResult { + FutureResult::new(async { Err(FlowyError::not_support()) }) + } + fn get_user_awareness_doc_state( &self, uid: i64, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 3f7aa93a61..b2c36d8c6b 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -650,7 +650,7 @@ pub async fn delete_workspace_member_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub async fn get_workspace_member_handler( +pub async fn get_workspace_members_handler( data: AFPluginData, manager: AFPluginState>, ) -> DataResult { diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index c1407254e1..42be2a256b 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -59,7 +59,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { // instead .event(UserEvent::GetMemberInfo, get_workspace_member_info) .event(UserEvent::RemoveWorkspaceMember, delete_workspace_member_handler) - .event(UserEvent::GetWorkspaceMember, get_workspace_member_handler) + .event(UserEvent::GetWorkspaceMembers, get_workspace_members_handler) .event(UserEvent::UpdateWorkspaceMember, update_workspace_member_handler) // Workspace .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) @@ -206,7 +206,7 @@ pub enum UserEvent { UpdateWorkspaceMember = 39, #[event(input = "QueryWorkspacePB", output = "RepeatedWorkspaceMemberPB")] - GetWorkspaceMember = 40, + GetWorkspaceMembers = 40, #[event(input = "ImportAppFlowyDataPB")] ImportAppFlowyDataFolder = 41, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index cf0de6c36b..cee00fecd4 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -366,6 +366,19 @@ impl UserManager { Ok(members) } + pub async fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> FlowyResult { + let member = self + .cloud_services + .get_user_service()? + .get_workspace_member(workspace_id, uid) + .await?; + Ok(member) + } + pub async fn update_workspace_member( &self, user_email: String, @@ -520,7 +533,7 @@ impl UserManager { let member = self .cloud_services .get_user_service()? - .get_workspace_member_info(&workspace_id, uid) + .get_workspace_member_info(workspace_id, uid) .await?; let record = WorkspaceMemberTable {