mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: start on AI plan+billing UI
This commit is contained in:
parent
6d0c9f766b
commit
7c77f0e9a9
@ -16,6 +16,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
const _buttonsMinWidth = 116.0;
|
||||
|
||||
class SettingsBillingView extends StatelessWidget {
|
||||
const SettingsBillingView({
|
||||
super.key,
|
||||
@ -57,7 +59,9 @@ class SettingsBillingView extends StatelessWidget {
|
||||
},
|
||||
ready: (state) {
|
||||
final billingPortalEnabled = state.billingPortal != null &&
|
||||
state.billingPortal!.url.isNotEmpty;
|
||||
state.billingPortal!.url.isNotEmpty &&
|
||||
state.subscription.subscriptionPlan !=
|
||||
SubscriptionPlanPB.None;
|
||||
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||
@ -77,6 +81,7 @@ class SettingsBillingView extends StatelessWidget {
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_plan_planButtonLabel
|
||||
.tr(),
|
||||
minWidth: _buttonsMinWidth,
|
||||
),
|
||||
if (billingPortalEnabled)
|
||||
SingleSettingAction(
|
||||
@ -89,6 +94,7 @@ class SettingsBillingView extends StatelessWidget {
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_plan_periodButtonLabel
|
||||
.tr(),
|
||||
minWidth: _buttonsMinWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -108,9 +114,34 @@ class SettingsBillingView extends StatelessWidget {
|
||||
buttonLabel: LocaleKeys
|
||||
.settings_billingPage_paymentDetails_methodButtonLabel
|
||||
.tr(),
|
||||
minWidth: _buttonsMinWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
// TODO(Mathias): Implement the business logic for AI Add-ons
|
||||
const SettingsCategory(
|
||||
title: 'Add-ons',
|
||||
children: [
|
||||
SingleSettingAction(
|
||||
buttonType: SingleSettingsButtonType.highlight,
|
||||
label: 'AppFlowy AI Max',
|
||||
description:
|
||||
"\$8 /user per month billed annually or \$10 billed monthly",
|
||||
buttonLabel: 'Add AI Max',
|
||||
fontWeight: FontWeight.w500,
|
||||
minWidth: _buttonsMinWidth,
|
||||
),
|
||||
SingleSettingAction(
|
||||
buttonType: SingleSettingsButtonType.highlight,
|
||||
label: 'AppFlowy AI Offline',
|
||||
description:
|
||||
"\$8 /user per month billed annually or \$10 billed monthly",
|
||||
buttonLabel: 'Add AI Offline',
|
||||
fontWeight: FontWeight.w500,
|
||||
minWidth: _buttonsMinWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -1,6 +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/util/int64_extension.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
@ -16,6 +15,7 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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/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';
|
||||
@ -69,9 +69,44 @@ class SettingsPlanView extends StatelessWidget {
|
||||
_PlanUsageSummary(
|
||||
usage: state.workspaceUsage,
|
||||
subscription: state.subscription,
|
||||
billingPortal: state.billingPortal,
|
||||
),
|
||||
const VSpace(16),
|
||||
_CurrentPlanBox(subscription: state.subscription),
|
||||
const VSpace(16),
|
||||
// TODO(Mathias): Localize and add business logic
|
||||
FlowyText(
|
||||
'Add-ons',
|
||||
fontSize: 18,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: _AddOnBox(
|
||||
title: "AI Max",
|
||||
description:
|
||||
"Unlimited AI responses with access to the latest advanced AI models.",
|
||||
price: "\$8",
|
||||
priceInfo: "billed annually or \$10 billed monthly",
|
||||
buttonText: "Add AI Max",
|
||||
),
|
||||
),
|
||||
HSpace(8),
|
||||
Flexible(
|
||||
child: _AddOnBox(
|
||||
title: "AI Offline",
|
||||
description:
|
||||
"Run AI locally on your device for maximum privacy.",
|
||||
price: "\$8",
|
||||
priceInfo: "billed annually or \$10 billed monthly",
|
||||
buttonText: "Add AI Offline",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -116,68 +151,67 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
||||
border: Border.all(color: const Color(0xFFBDBDBD)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(4),
|
||||
FlowyText.semibold(
|
||||
widget.subscription.label,
|
||||
fontSize: 24,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
const VSpace(8),
|
||||
FlowyText.regular(
|
||||
widget.subscription.info,
|
||||
fontSize: 16,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
maxLines: 3,
|
||||
),
|
||||
const VSpace(16),
|
||||
FlowyGradientButton(
|
||||
label: LocaleKeys
|
||||
.settings_planPage_planUsage_currentPlan_upgrade
|
||||
.tr(),
|
||||
onPressed: () => _openPricingDialog(
|
||||
context,
|
||||
context.read<SettingsPlanBloc>().workspaceId,
|
||||
widget.subscription,
|
||||
),
|
||||
),
|
||||
if (widget.subscription.hasCanceled) ...[
|
||||
const VSpace(12),
|
||||
FlowyText(
|
||||
LocaleKeys
|
||||
.settings_planPage_planUsage_currentPlan_canceledInfo
|
||||
.tr(
|
||||
args: [_canceledDate(context)],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(4),
|
||||
FlowyText.semibold(
|
||||
widget.subscription.label,
|
||||
fontSize: 24,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
maxLines: 5,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const HSpace(16),
|
||||
Expanded(
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(4),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._getPros(widget.subscription.subscriptionPlan).map(
|
||||
(s) => _ProConItem(label: s),
|
||||
const VSpace(8),
|
||||
FlowyText.regular(
|
||||
widget.subscription.info,
|
||||
fontSize: 16,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
..._getCons(widget.subscription.subscriptionPlan).map(
|
||||
(s) => _ProConItem(label: s, isPro: false),
|
||||
),
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 220),
|
||||
child: FlowyGradientButton(
|
||||
label: LocaleKeys
|
||||
.settings_planPage_planUsage_currentPlan_upgrade
|
||||
.tr(),
|
||||
onPressed: () => _openPricingDialog(
|
||||
context,
|
||||
context.read<SettingsPlanBloc>().workspaceId,
|
||||
widget.subscription,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.subscription.hasCanceled) ...[
|
||||
const VSpace(12),
|
||||
FlowyText(
|
||||
LocaleKeys
|
||||
.settings_planPage_planUsage_currentPlan_canceledInfo
|
||||
.tr(
|
||||
args: [_canceledDate(context)],
|
||||
),
|
||||
maxLines: 5,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -185,14 +219,21 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: const BoxDecoration(color: Color(0xFF4F3F5F)),
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF4F3F5F),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_bannerLabel
|
||||
.tr(),
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
@ -226,97 +267,18 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
List<String> _getPros(SubscriptionPlanPB plan) => switch (plan) {
|
||||
SubscriptionPlanPB.Pro => _proPros(),
|
||||
_ => _freePros(),
|
||||
};
|
||||
|
||||
List<String> _getCons(SubscriptionPlanPB plan) => switch (plan) {
|
||||
SubscriptionPlanPB.Pro => _proCons(),
|
||||
_ => _freeCons(),
|
||||
};
|
||||
|
||||
List<String> _freePros() => [
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProOne.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProTwo.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProThree.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFour.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFive.tr(),
|
||||
];
|
||||
|
||||
List<String> _freeCons() => [
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConOne.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConTwo.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConThree.tr(),
|
||||
];
|
||||
|
||||
List<String> _proPros() => [
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProOne
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProTwo
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProThree
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFour
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFive
|
||||
.tr(),
|
||||
];
|
||||
|
||||
List<String> _proCons() => [
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConOne
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConTwo
|
||||
.tr(),
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConThree
|
||||
.tr(),
|
||||
];
|
||||
}
|
||||
|
||||
class _ProConItem extends StatelessWidget {
|
||||
const _ProConItem({
|
||||
required this.label,
|
||||
this.isPro = true,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isPro;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
child: FlowySvg(
|
||||
isPro ? FlowySvgs.check_m : FlowySvgs.close_error_s,
|
||||
size: const Size.square(18),
|
||||
color: isPro
|
||||
? AFThemeExtension.of(context).strongText
|
||||
: const Color(0xFF900000),
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
label,
|
||||
fontSize: 12,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanUsageSummary extends StatelessWidget {
|
||||
const _PlanUsageSummary({required this.usage, required this.subscription});
|
||||
const _PlanUsageSummary({
|
||||
required this.usage,
|
||||
required this.subscription,
|
||||
this.billingPortal,
|
||||
});
|
||||
|
||||
final WorkspaceUsagePB usage;
|
||||
final WorkspaceSubscriptionPB subscription;
|
||||
final BillingPortalPB? billingPortal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -372,21 +334,29 @@ class _PlanUsageSummary extends StatelessWidget {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_ToggleMore(
|
||||
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
|
||||
subscription: subscription,
|
||||
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
||||
),
|
||||
const VSpace(8),
|
||||
_ToggleMore(
|
||||
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_guestCollabToggle.tr(),
|
||||
subscription: subscription,
|
||||
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
||||
),
|
||||
if (subscription.subscriptionPlan == SubscriptionPlanPB.None) ...[
|
||||
_ToggleMore(
|
||||
value: false,
|
||||
label:
|
||||
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
|
||||
subscription: subscription,
|
||||
badgeLabel:
|
||||
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
|
||||
onTap: billingPortal?.url == null
|
||||
? null
|
||||
: () async {
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent.addSubscription(
|
||||
SubscriptionPlanPB.Pro,
|
||||
),
|
||||
);
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -445,12 +415,14 @@ class _ToggleMore extends StatefulWidget {
|
||||
required this.label,
|
||||
required this.subscription,
|
||||
this.badgeLabel,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final bool value;
|
||||
final String label;
|
||||
final WorkspaceSubscriptionPB subscription;
|
||||
final String? badgeLabel;
|
||||
final Future<void> Function()? onTap;
|
||||
|
||||
@override
|
||||
State<_ToggleMore> createState() => _ToggleMoreState();
|
||||
@ -472,29 +444,17 @@ class _ToggleMoreState extends State<_ToggleMore> {
|
||||
Toggle(
|
||||
value: toggleValue,
|
||||
padding: EdgeInsets.zero,
|
||||
onChanged: (_) {
|
||||
setState(() => toggleValue = !toggleValue);
|
||||
onChanged: (_) async {
|
||||
if (widget.onTap == null || toggleValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider<SettingsPlanBloc>.value(
|
||||
value: context.read<SettingsPlanBloc>(),
|
||||
child: SettingsPlanComparisonDialog(
|
||||
workspaceId: context.read<SettingsPlanBloc>().workspaceId,
|
||||
subscription: widget.subscription,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (mounted) {
|
||||
setState(() => toggleValue = !toggleValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
setState(() => toggleValue = !toggleValue);
|
||||
await widget.onTap!();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => toggleValue = !toggleValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
const HSpace(10),
|
||||
@ -575,6 +535,104 @@ class _PlanProgressIndicator extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AddOnBox extends StatelessWidget {
|
||||
const _AddOnBox({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.price,
|
||||
required this.priceInfo,
|
||||
required this.buttonText,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String price;
|
||||
final String priceInfo;
|
||||
final String buttonText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 200,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFBDBDBD)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
title,
|
||||
fontSize: 14,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
const VSpace(4),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
description,
|
||||
fontSize: 11,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
maxLines: 4,
|
||||
),
|
||||
const VSpace(4),
|
||||
Row(
|
||||
children: [
|
||||
FlowyText(
|
||||
price,
|
||||
fontSize: 24,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
const HSpace(4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: FlowyText(
|
||||
'/user per month',
|
||||
fontSize: 11,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
priceInfo,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
fontSize: 11,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
FlowyTextButton(
|
||||
buttonText,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
||||
fillColor: Colors.transparent,
|
||||
radius: Corners.s16Border,
|
||||
hoverColor: const Color(0xFF5C3699),
|
||||
fontColor: const Color(0xFF5C3699),
|
||||
fontHoverColor: Colors.white,
|
||||
borderColor: const Color(0xFF5C3699),
|
||||
fontSize: 12,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Uncomment if we need it in the future
|
||||
// class _DealBox extends StatelessWidget {
|
||||
// const _DealBox();
|
||||
|
@ -207,7 +207,7 @@ class SettingsWorkspaceView extends StatelessWidget {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
).show(context),
|
||||
isDangerous: true,
|
||||
buttonType: SingleSettingsButtonType.danger,
|
||||
buttonLabel: workspaceMember?.role.isOwner ?? false
|
||||
? LocaleKeys
|
||||
.settings_workspacePage_manageWorkspace_deleteWorkspace
|
||||
|
@ -71,7 +71,7 @@ class _FlowyGradientButtonState extends State<FlowyGradientButton> {
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: FlowyText(
|
||||
widget.label,
|
||||
fontSize: 16,
|
||||
|
@ -6,6 +6,16 @@ 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';
|
||||
|
||||
enum SingleSettingsButtonType {
|
||||
primary,
|
||||
danger,
|
||||
highlight;
|
||||
|
||||
bool get isPrimary => this == primary;
|
||||
bool get isDangerous => this == danger;
|
||||
bool get isHighlight => this == highlight;
|
||||
}
|
||||
|
||||
/// This is used to describe a single setting action
|
||||
///
|
||||
/// This will render a simple action that takes the title,
|
||||
@ -18,15 +28,18 @@ class SingleSettingAction extends StatelessWidget {
|
||||
const SingleSettingAction({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.description,
|
||||
this.labelMaxLines,
|
||||
required this.buttonLabel,
|
||||
this.onPressed,
|
||||
this.isDangerous = false,
|
||||
this.buttonType = SingleSettingsButtonType.primary,
|
||||
this.fontSize = 14,
|
||||
this.fontWeight = FontWeight.normal,
|
||||
this.minWidth,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? description;
|
||||
final int? labelMaxLines;
|
||||
final String buttonLabel;
|
||||
|
||||
@ -36,46 +49,115 @@ class SingleSettingAction extends StatelessWidget {
|
||||
///
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// If isDangerous is true, the button will be rendered as a dangerous
|
||||
/// action, with a red outline.
|
||||
///
|
||||
final bool isDangerous;
|
||||
final SingleSettingsButtonType buttonType;
|
||||
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final double? minWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
label,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
maxLines: labelMaxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
label,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
maxLines: labelMaxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (description != null) ...[
|
||||
const VSpace(4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
description!,
|
||||
fontSize: 11,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const HSpace(24),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: minWidth ?? 0.0,
|
||||
maxHeight: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
child: FlowyTextButton(
|
||||
buttonLabel,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
|
||||
fillColor:
|
||||
isDangerous ? null : Theme.of(context).colorScheme.primary,
|
||||
radius: Corners.s12Border,
|
||||
hoverColor: isDangerous ? null : const Color(0xFF005483),
|
||||
fontColor: isDangerous ? Theme.of(context).colorScheme.error : null,
|
||||
fontHoverColor: Colors.white,
|
||||
fillColor: fillColor(context),
|
||||
radius: Corners.s8Border,
|
||||
hoverColor: hoverColor(context),
|
||||
fontColor: fontColor(context),
|
||||
fontHoverColor: fontHoverColor(context),
|
||||
borderColor: borderColor(context),
|
||||
fontSize: 12,
|
||||
isDangerous: isDangerous,
|
||||
isDangerous: buttonType.isDangerous,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color? fillColor(BuildContext context) {
|
||||
if (buttonType.isPrimary) {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
Color? hoverColor(BuildContext context) {
|
||||
if (buttonType.isPrimary) {
|
||||
return const Color(0xFF005483);
|
||||
}
|
||||
|
||||
if (buttonType.isHighlight) {
|
||||
return const Color(0xFF5C3699);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Color? fontColor(BuildContext context) {
|
||||
if (buttonType.isDangerous) {
|
||||
return Theme.of(context).colorScheme.error;
|
||||
}
|
||||
|
||||
if (buttonType.isHighlight) {
|
||||
return const Color(0xFF5C3699);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Color? fontHoverColor(BuildContext context) {
|
||||
return Colors.white;
|
||||
}
|
||||
|
||||
Color? borderColor(BuildContext context) {
|
||||
if (buttonType.isHighlight) {
|
||||
return const Color(0xFF5C3699);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -82,4 +82,7 @@ class Corners {
|
||||
|
||||
static const BorderRadius s12Border = BorderRadius.all(s12Radius);
|
||||
static const Radius s12Radius = Radius.circular(12);
|
||||
|
||||
static const BorderRadius s16Border = BorderRadius.all(s16Radius);
|
||||
static const Radius s16Radius = Radius.circular(16);
|
||||
}
|
||||
|
@ -165,6 +165,7 @@ class FlowyTextButton extends StatelessWidget {
|
||||
this.decoration,
|
||||
this.fontFamily,
|
||||
this.isDangerous = false,
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
final String text;
|
||||
@ -188,6 +189,7 @@ class FlowyTextButton extends StatelessWidget {
|
||||
|
||||
final String? fontFamily;
|
||||
final bool isDangerous;
|
||||
final Color? borderColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -222,9 +224,10 @@ class FlowyTextButton extends StatelessWidget {
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: isDangerous
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Colors.transparent,
|
||||
color: borderColor ??
|
||||
(isDangerous
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Colors.transparent),
|
||||
),
|
||||
borderRadius: radius ?? Corners.s6Border,
|
||||
),
|
||||
|
@ -636,8 +636,7 @@
|
||||
"aiResponseLabel": "AI Responses",
|
||||
"aiResponseUsage": "{} of {}",
|
||||
"proBadge": "Pro",
|
||||
"memberProToggle": "Unlimited members",
|
||||
"guestCollabToggle": "10 guest collaborators",
|
||||
"memberProToggle": "10 members and unlimited AI responses",
|
||||
"storageUnlimited": "Unlimited storage with your Pro Plan",
|
||||
"aiCredit": {
|
||||
"title": "Add @:appName AI Credit",
|
||||
@ -656,23 +655,7 @@
|
||||
"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)",
|
||||
"freeProThree": "Unlimited guests (view-only)",
|
||||
"freeProFour": "Storage 5GB",
|
||||
"freeProFive": "30 day revision history",
|
||||
"freeConOne": "Guest collaborators (edit access)",
|
||||
"freeConTwo": "Unlimited storage",
|
||||
"freeConThree": "6 month revision history",
|
||||
"professionalProOne": "Collaborative workspace",
|
||||
"professionalProTwo": "Unlimited members",
|
||||
"professionalProThree": "Unlimited guests (view-only)",
|
||||
"professionalProFour": "Unlimited storage",
|
||||
"professionalProFive": "6 month revision history",
|
||||
"professionalConOne": "Unlimited guest collaborators (edit access)",
|
||||
"professionalConTwo": "Unlimited AI responses",
|
||||
"professionalConThree": "1 year revision history",
|
||||
"upgrade": "Upgrade plan",
|
||||
"canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}."
|
||||
},
|
||||
"deal": {
|
||||
@ -2031,4 +2014,4 @@
|
||||
"movePageToSpace": "Move page to space",
|
||||
"switchSpace": "Switch space"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user