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
This commit is contained in:
Mathias Mogensen 2024-06-17 14:30:19 +02:00 committed by GitHub
parent 8bf97ad5c6
commit 4a126e17ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 857 additions and 410 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -130,12 +130,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
await openPage('v020', layout: ViewLayoutPB.Grid);
}
Future<void> hoverOnFirstRowOfGrid() async {
Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {
final findRow = find.byType(GridRow);
expect(findRow, findsWidgets);
final firstRow = findRow.first;
await hoverOnWidget(firstRow);
await hoverOnWidget(firstRow, onHover: onHover);
}
Future<void> editCell({
@ -876,11 +876,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapRowMenuButtonInGrid() async {
expect(find.byType(RowMenuButton), findsOneWidget);
await tapButton(find.byType(RowMenuButton));
}
/// Should call [tapRowMenuButtonInGrid] first.
Future<void> tapDeleteOnRowMenu() async {
expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget);
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
}

View File

@ -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<FieldType?> showFieldTypeGridBottomSheet(

View File

@ -49,9 +49,7 @@ class DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {
on<DatabaseGroupEvent>(
(event, emit) async {
await event.when(
initial: () {
_startListening();
},
initial: () async => _startListening(),
didReceiveFieldUpdate: (fieldInfos) {
emit(
state.copyWith(

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Relation,
FieldType.Summary,
FieldType.Time,
// FieldType.Time,
FieldType.Translate,
];

View File

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

View File

@ -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<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
Future<FlowyResult<PaymentLinkPB, FlowyError>> 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<FlowyResult<void, FlowyError>> addWorkspaceMember(
@ -225,20 +233,29 @@ class UserBackendService {
return UserEventGetWorkspaceSubscriptions().send();
}
static Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
getWorkspaceMember() async {
final data = WorkspaceMemberIdPB.create()..uid = userId;
return UserEventGetMemberInfo(data).send();
}
@override
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
String workspaceId,
SubscriptionPlanPB plan,
) {
final request = SubscribeWorkspacePB()
..workspaceId = workspaceId
..recurringInterval = RecurringIntervalPB.Month
..recurringInterval = RecurringIntervalPB.Year
..workspaceSubscriptionPlan = plan
..successUrl =
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
return UserEventSubscribeWorkspace(request).send();
}
static Future<FlowyResult<void, FlowyError>> cancelSubscription(
@override
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
) {
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;

View File

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

View File

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

View File

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

View File

@ -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<SettingsPlanEvent, SettingsPlanState> {
SettingsPlanBloc({
required this.workspaceId,
required Int64 userId,
}) : super(const _Initial()) {
_service = WorkspaceService(workspaceId: workspaceId);
_userService = UserBackendService(userId: userId);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
@ -103,7 +106,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
}
},
addSubscription: (plan) async {
final result = await UserBackendService.createSubscription(
final result = await _userService.createSubscription(
workspaceId,
SubscriptionPlanPB.Pro,
);
@ -114,7 +117,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
);
},
cancelSubscription: () async {
await UserBackendService.cancelSubscription(workspaceId);
await _userService.cancelSubscription(workspaceId);
add(const SettingsPlanEvent.started());
},
paymentSuccessful: () {
@ -131,6 +134,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
late final String workspaceId;
late final WorkspaceService _service;
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;
void _onPaymentSuccessful() {

View File

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

View File

@ -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<WorkspaceMemberPB> members,
@Default(AFRolePB.Guest) AFRolePB myRole,
@Default(false) bool deleteWorkspace,
@Default(false) bool leaveWorkspace,
}) = _WorkspaceSettingsState;

View File

@ -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<UserWorkspaceEvent, UserWorkspaceState> {
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<UserWorkspaceEvent, UserWorkspaceState> {
(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<UserWorkspaceEvent, UserWorkspaceState> {
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<UserWorkspacePB> workspaces,
@Default(null) WorkspaceMemberPB? currentWorkspaceMember,
@Default(null) UserWorkspaceActionResult? actionResult,
@Default(false) bool isCollabWorkspaceOn,
}) = _UserWorkspaceState;

View File

@ -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<UserSettingButton> createState() => _UserSettingButtonState();
}
class _UserSettingButtonState extends State<UserSettingButton> {
late UserWorkspaceBloc _userWorkspaceBloc;
@override
void initState() {
super.initState();
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
}
@override
void didChangeDependencies() {
_userWorkspaceBloc = context.read<UserWorkspaceBloc>();
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<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
),
BlocProvider.value(value: context.read<UserWorkspaceBloc>()),
BlocProvider.value(value: bloc ?? context.read<UserWorkspaceBloc>()),
],
child: SettingsDialog(
userProfile,
workspaceId: context
.read<UserWorkspaceBloc>()
.state
.currentWorkspace!
.workspaceId,
didLogout: () async {
// Pop the dialog using the dialog context
Navigator.of(dialogContext).pop();

View File

@ -58,7 +58,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
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(),

View File

@ -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<bool?>(
context: context,
builder: (_) => BlocProvider<SettingsPlanBloc>(
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,

View File

@ -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<SettingsPlanBloc>().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<String> 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(),
),
];

View File

@ -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<SettingsPlanBloc>(
create: (context) => SettingsPlanBloc(workspaceId: workspaceId)
..add(const SettingsPlanEvent.started()),
create: (context) => SettingsPlanBloc(
workspaceId: workspaceId,
userId: user.id,
)..add(const SettingsPlanEvent.started()),
child: BlocBuilder<SettingsPlanBloc, SettingsPlanState>(
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(

View File

@ -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<AppearanceSettingsCubit>().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<WorkspaceSettingsBloc>().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<WorkspaceSettingsBloc>().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<WorkspaceSettingsBloc>()
.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<AppearanceSettingsCubit>().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<AppearanceSettingsCubit>()
.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<AppearanceSettingsCubit>()
.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<String> 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<String> _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<AppearanceSettingsCubit>()
.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();

View File

@ -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<SettingsDialogBloc>().state.page,
member: context
.read<UserWorkspaceBloc>()
.state
.currentWorkspaceMember,
),
),
Expanded(
child: getSettingsView(
context
.read<UserWorkspaceBloc>()
.state
.currentWorkspace!
.workspaceId,
context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.userProfile,
context
.read<UserWorkspaceBloc>()
.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:

View File

@ -71,7 +71,7 @@ class _FlowyGradientButtonState extends State<FlowyGradientButton> {
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 8),
child: FlowyText(
widget.label,
fontSize: 16,

View File

@ -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<T> extends State<SettingsDropdown<T>> {
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)),

View File

@ -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<SettingsInputField> createState() => _SettingsInputFieldState();
@ -127,7 +130,10 @@ class _SettingsInputFieldState extends State<SettingsInputField> {
),
),
onSubmitted: widget.onSave,
onChanged: (_) => setState(() {}),
onChanged: (_) {
widget.onChanged?.call(controller.text);
setState(() {});
},
),
),
if (!widget.hideActions &&

View File

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

View File

@ -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<AppFlowyCloudSettingBloc>().add(
AppFlowyCloudSettingEvent.enableSync(value),
);
},
activeColor: Theme.of(context).colorScheme.primary,
Toggle(
style: ToggleStyle.big,
value: state.setting.enableSync,
onChanged: (value) => context
.read<AppFlowyCloudSettingBloc>()
.add(AppFlowyCloudSettingEvent.enableSync(!value)),
),
],
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
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<AFThemeExtension> {
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<AFThemeExtension> {
Color? tint9,
Color? textColor,
Color? secondaryTextColor,
Color? strongText,
Color? calloutBGColor,
Color? tableCellBGColor,
Color? greyHover,
@ -117,6 +120,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
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<AFThemeExtension> {
other.secondaryTextColor,
t,
)!,
strongText: Color.lerp(
strongText,
other.strongText,
t,
)!,
calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!,
tableCellBGColor:
Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!,

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FlowyText extends StatelessWidget {

View File

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

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3503_8832" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_3503_8832)">
<path d="M9.54998 18L3.84998 12.3L5.27498 10.875L9.54998 15.15L18.725 5.97501L20.15 7.40001L9.54998 18Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 431 B

View File

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

View File

@ -85,7 +85,7 @@ impl EventIntegrationTest {
pub async fn get_workspace_members(&self, workspace_id: &str) -> Vec<WorkspaceMemberPB> {
EventBuilder::new(self.clone())
.event(UserEvent::GetWorkspaceMember)
.event(UserEvent::GetWorkspaceMembers)
.payload(QueryWorkspacePB {
workspace_id: workspace_id.to_string(),
})

View File

@ -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<FolderIndexManagerImpl>,
cloud_service: Arc<dyn SearchCloudService>,
folder_manager: Arc<FolderManager>,
_cloud_service: Arc<dyn SearchCloudService>,
_folder_manager: Arc<FolderManager>,
) -> Arc<SearchManager> {
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]))
}
}

View File

@ -339,6 +339,23 @@ where
})
}
fn get_workspace_member(
&self,
workspace_id: String,
uid: i64,
) -> FutureResult<WorkspaceMember, FlowyError> {
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,

View File

@ -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<WorkspaceMember, FlowyError> {
FutureResult::new(async { Err(FlowyError::not_support()) })
}
fn get_user_awareness_doc_state(
&self,
uid: i64,

View File

@ -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<QueryWorkspacePB>,
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<RepeatedWorkspaceMemberPB, FlowyError> {

View File

@ -59,7 +59,7 @@ pub fn init(user_manager: Weak<UserManager>) -> 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,

View File

@ -366,6 +366,19 @@ impl UserManager {
Ok(members)
}
pub async fn get_workspace_member(
&self,
workspace_id: String,
uid: i64,
) -> FlowyResult<WorkspaceMember> {
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 {