mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
8bf97ad5c6
commit
4a126e17ce
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -22,7 +22,7 @@ const List<FieldType> _supportedFieldTypes = [
|
||||
FieldType.CreatedTime,
|
||||
FieldType.Relation,
|
||||
FieldType.Summary,
|
||||
FieldType.Time,
|
||||
// FieldType.Time,
|
||||
FieldType.Translate,
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
),
|
||||
];
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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)),
|
||||
|
@ -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 &&
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)!,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class FlowyText extends StatelessWidget {
|
||||
|
@ -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,
|
||||
|
8
frontend/resources/flowy_icons/24x/check.svg
Normal file
8
frontend/resources/flowy_icons/24x/check.svg
Normal 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 |
@ -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",
|
||||
|
@ -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(),
|
||||
})
|
||||
|
@ -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]))
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user