mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: enable billing (#5779)
* chore: enable billing * chore: adjust bright mode UI * chore: show corresponding error in sidebar * chore: dismiss dialog in ai writter when hit ai response * fix: improvements from test session * chore: ai error message for database * chore: different prompt for workspace owner * feat: cancel plan survey * chore: show ai repsonse limit on chat * fix: sidebar toast after merge * chore: remove unused debug print * fix: popover close on action * fix: minor copy changes * chore: disable billing * chore: disbale billing --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
This commit is contained in:
parent
b5d799655a
commit
4a5eda6eeb
@ -17,17 +17,23 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
required this.questionId,
|
||||
}) : super(ChatAIMessageState.initial(message)) {
|
||||
if (state.stream != null) {
|
||||
_subscription = state.stream!.listen((text) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
_subscription = state.stream!.listen(
|
||||
onData: (text) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.updateText(text));
|
||||
}
|
||||
|
||||
if (text.startsWith("data:")) {
|
||||
add(ChatAIMessageEvent.newText(text.substring(5)));
|
||||
} else if (text.startsWith("error:")) {
|
||||
add(ChatAIMessageEvent.receiveError(text.substring(5)));
|
||||
},
|
||||
onError: (error) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.receiveError(error.toString()));
|
||||
}
|
||||
});
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (state.stream!.error != null) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
@ -42,11 +48,16 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
newText: (newText) {
|
||||
emit(state.copyWith(text: state.text + newText, error: null));
|
||||
updateText: (newText) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: newText,
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
receiveError: (error) {
|
||||
emit(state.copyWith(error: error));
|
||||
emit(state.copyWith(messageState: MessageState.onError(error)));
|
||||
},
|
||||
retry: () {
|
||||
if (questionId is! Int64) {
|
||||
@ -55,8 +66,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
retryState: const LoadingState.loading(),
|
||||
error: null,
|
||||
messageState: const MessageState.loading(),
|
||||
),
|
||||
);
|
||||
|
||||
@ -82,8 +92,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: text,
|
||||
error: null,
|
||||
retryState: const LoadingState.finish(),
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: const MessageState.onAIResponseLimit(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -98,7 +114,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement>? _subscription;
|
||||
StreamSubscription<String>? _subscription;
|
||||
final String chatId;
|
||||
final Int64? questionId;
|
||||
}
|
||||
@ -106,26 +122,34 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
@freezed
|
||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.initial() = Initial;
|
||||
const factory ChatAIMessageEvent.newText(String text) = _NewText;
|
||||
const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;
|
||||
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
|
||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatAIMessageState with _$ChatAIMessageState {
|
||||
const factory ChatAIMessageState({
|
||||
AnswerStream? stream,
|
||||
String? error,
|
||||
required String text,
|
||||
required LoadingState retryState,
|
||||
required MessageState messageState,
|
||||
}) = _ChatAIMessageState;
|
||||
|
||||
factory ChatAIMessageState.initial(dynamic text) {
|
||||
return ChatAIMessageState(
|
||||
text: text is String ? text : "",
|
||||
stream: text is AnswerStream ? text : null,
|
||||
retryState: const LoadingState.finish(),
|
||||
messageState: const MessageState.ready(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MessageState with _$MessageState {
|
||||
const factory MessageState.onError(String error) = _Error;
|
||||
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
|
||||
const factory MessageState.ready() = _Ready;
|
||||
const factory MessageState.loading() = _Loading;
|
||||
}
|
||||
|
@ -525,8 +525,6 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
typedef AnswerStreamElement = String;
|
||||
|
||||
class AnswerStream {
|
||||
AnswerStream() {
|
||||
_port.handler = _controller.add;
|
||||
@ -534,23 +532,53 @@ class AnswerStream {
|
||||
(event) {
|
||||
if (event.startsWith("data:")) {
|
||||
_hasStarted = true;
|
||||
final newText = event.substring(5);
|
||||
_text += newText;
|
||||
if (_onData != null) {
|
||||
_onData!(_text);
|
||||
}
|
||||
} else if (event.startsWith("error:")) {
|
||||
_error = event.substring(5);
|
||||
if (_onError != null) {
|
||||
_onError!(_error!);
|
||||
}
|
||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||
if (_onAIResponseLimit != null) {
|
||||
_onAIResponseLimit!();
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (_onEnd != null) {
|
||||
_onEnd!();
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (_onError != null) {
|
||||
_onError!(error.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<AnswerStreamElement> _controller =
|
||||
StreamController.broadcast();
|
||||
late StreamSubscription<AnswerStreamElement> _subscription;
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<String> _subscription;
|
||||
bool _hasStarted = false;
|
||||
String? _error;
|
||||
String _text = "";
|
||||
|
||||
// Callbacks
|
||||
void Function(String text)? _onData;
|
||||
void Function()? _onStart;
|
||||
void Function()? _onEnd;
|
||||
void Function(String error)? _onError;
|
||||
void Function()? _onAIResponseLimit;
|
||||
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
bool get hasStarted => _hasStarted;
|
||||
String? get error => _error;
|
||||
String get text => _text;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
@ -558,9 +586,23 @@ class AnswerStream {
|
||||
_port.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement> listen(
|
||||
void Function(AnswerStreamElement event)? onData,
|
||||
) {
|
||||
return _controller.stream.listen(onData);
|
||||
StreamSubscription<String> listen({
|
||||
void Function(String text)? onData,
|
||||
void Function()? onStart,
|
||||
void Function()? onEnd,
|
||||
void Function(String error)? onError,
|
||||
void Function()? onAIResponseLimit,
|
||||
}) {
|
||||
_onData = onData;
|
||||
_onStart = onStart;
|
||||
_onEnd = onEnd;
|
||||
_onError = onError;
|
||||
_onAIResponseLimit = onAIResponseLimit;
|
||||
|
||||
if (_onStart != null) {
|
||||
_onStart!();
|
||||
}
|
||||
|
||||
return _subscription;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -38,7 +37,8 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
||||
)..add(const ChatAIMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
if (state.error != null) {
|
||||
return state.messageState.when(
|
||||
onError: (err) {
|
||||
return StreamingError(
|
||||
onRetryPressed: () {
|
||||
context.read<ChatAIMessageBloc>().add(
|
||||
@ -46,18 +46,26 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (state.retryState == const LoadingState.loading()) {
|
||||
return const ChatAILoading();
|
||||
}
|
||||
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
return FlowyText(
|
||||
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
||||
maxLines: 10,
|
||||
lineHeight: 1.5,
|
||||
);
|
||||
},
|
||||
ready: () {
|
||||
if (state.text.isEmpty) {
|
||||
return const ChatAILoading();
|
||||
} else {
|
||||
return AIMarkdownText(markdown: state.text);
|
||||
}
|
||||
},
|
||||
loading: () {
|
||||
return const ChatAILoading();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
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';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
@ -7,19 +10,18 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableSummaryCellSkin {
|
||||
@ -149,7 +151,22 @@ class SummaryCellAccessory extends StatelessWidget {
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
child: BlocConsumer<SummaryRowBloc, SummaryRowState>(
|
||||
listenWhen: (previous, current) {
|
||||
return previous.error != current.error;
|
||||
},
|
||||
listener: (context, state) {
|
||||
if (state.error != null) {
|
||||
if (state.error!.isAIResponseLimitExceeded) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
);
|
||||
} else {
|
||||
showSnackBarMessage(context, state.error!.msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [SummaryButton(), HSpace(6), CopyButton()],
|
||||
@ -169,13 +186,13 @@ class SummaryButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) {
|
||||
return state.loadingState.when(
|
||||
loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
finish: (_) {
|
||||
finish: () {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
||||
child: Container(
|
||||
|
@ -1,3 +1,6 @@
|
||||
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';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
@ -7,19 +10,18 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableTranslateCellSkin {
|
||||
@ -150,7 +152,22 @@ class TranslateCellAccessory extends StatelessWidget {
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<TranslateRowBloc, TranslateRowState>(
|
||||
child: BlocConsumer<TranslateRowBloc, TranslateRowState>(
|
||||
listenWhen: (previous, current) {
|
||||
return previous.error != current.error;
|
||||
},
|
||||
listener: (context, state) {
|
||||
if (state.error != null) {
|
||||
if (state.error!.isAIResponseLimitExceeded) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
);
|
||||
} else {
|
||||
showSnackBarMessage(context, state.error!.msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [TranslateButton(), HSpace(6), CopyButton()],
|
||||
|
@ -6,8 +6,8 @@ import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -65,7 +65,7 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
||||
return (s.url, null);
|
||||
},
|
||||
(err) {
|
||||
if (err.code == ErrorCode.FileStorageLimitExceeded) {
|
||||
if (err.isStorageLimitExceeded) {
|
||||
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
|
||||
} else {
|
||||
return (null, err.msg);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
@ -7,7 +9,6 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OpenAIImageWidget extends StatefulWidget {
|
||||
const OpenAIImageWidget({
|
||||
|
@ -1,35 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class _AILimitDialog extends StatelessWidget {
|
||||
const _AILimitDialog({
|
||||
required this.message,
|
||||
required this.onOkPressed,
|
||||
});
|
||||
final VoidCallback onOkPressed;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigatorOkCancelDialog(
|
||||
message: message,
|
||||
okTitle: LocaleKeys.button_ok.tr(),
|
||||
onOkPressed: onOkPressed,
|
||||
titleUpperCase: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showAILimitDialog(BuildContext context, String message) {
|
||||
showDialog(
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (dialogContext) => _AILimitDialog(
|
||||
message: message,
|
||||
onOkPressed: () {},
|
||||
),
|
||||
title: LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
description: message,
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
|
||||
@ -10,12 +12,7 @@ import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'ai_limit_dialog.dart';
|
||||
@ -46,7 +43,7 @@ Node autoCompletionNode({
|
||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||
getName: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr,
|
||||
iconData: Icons.generating_tokens,
|
||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
||||
keywords: ['ai', 'openai', 'writer', 'ai writer', 'autogenerator'],
|
||||
nodeBuilder: (editorState, _) {
|
||||
final node = autoCompletionNode(start: editorState.selection!);
|
||||
return node;
|
||||
@ -130,7 +127,6 @@ class _AutoCompletionBlockComponentState
|
||||
_unsubscribeSelectionGesture();
|
||||
controller.dispose();
|
||||
textFieldFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -181,9 +177,7 @@ class _AutoCompletionBlockComponentState
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
recordUndo: false,
|
||||
),
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
|
||||
@ -230,6 +224,7 @@ class _AutoCompletionBlockComponentState
|
||||
if (mounted) {
|
||||
if (error.isLimitExceeded) {
|
||||
showAILimitDialog(context, error.message);
|
||||
await _onDiscard();
|
||||
} else {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
@ -417,12 +412,10 @@ class _AutoCompletionBlockComponentState
|
||||
// show dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => _onDiscard(),
|
||||
builder: (_) => DiscardDialog(
|
||||
onConfirm: _onDiscard,
|
||||
onCancel: () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (controller.text.isEmpty) {
|
||||
_onExit();
|
||||
@ -445,9 +438,7 @@ class _AutoCompletionBlockComponentState
|
||||
}
|
||||
|
||||
class AutoCompletionHeader extends StatelessWidget {
|
||||
const AutoCompletionHeader({
|
||||
super.key,
|
||||
});
|
||||
const AutoCompletionHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -471,23 +462,27 @@ class AutoCompletionInputFooter extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_generate.tr(),
|
||||
FlowyTextButton.primary(
|
||||
text: LocaleKeys.button_generate.tr(),
|
||||
context: context,
|
||||
onPressed: onGenerate,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
context: context,
|
||||
onPressed: onExit,
|
||||
),
|
||||
Expanded(
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -512,18 +507,21 @@ class AutoCompletionFooter extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_keep.tr(),
|
||||
FlowyTextButton.primary(
|
||||
context: context,
|
||||
text: LocaleKeys.button_keep.tr(),
|
||||
onPressed: onKeep,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
context: context,
|
||||
text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||
onPressed: onRewrite,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_discard.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
context: context,
|
||||
text: LocaleKeys.button_discard.tr(),
|
||||
onPressed: onDiscard,
|
||||
),
|
||||
],
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
@ -23,7 +25,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@ -239,19 +240,15 @@ class PageStyleCoverImage extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
if (result == null) {
|
||||
showSnapBar(
|
||||
return showSnapBar(
|
||||
context,
|
||||
LocaleKeys.document_plugins_image_imageUploadFailed,
|
||||
LocaleKeys.document_plugins_image_imageUploadFailed.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<DocumentPageStyleBloc>().add(
|
||||
DocumentPageStyleEvent.updateCoverImage(
|
||||
PageStyleCover(
|
||||
type: type,
|
||||
value: result,
|
||||
),
|
||||
PageStyleCover(type: type, value: result),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -282,10 +279,7 @@ class PageStyleCoverImage extends StatelessWidget {
|
||||
},
|
||||
builder: (_) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
minHeight: 80,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80),
|
||||
child: BlocProvider.value(
|
||||
value: pageStyleBloc,
|
||||
child: Padding(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
@ -6,7 +8,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ShareMenuButton extends StatelessWidget {
|
||||
|
@ -91,7 +91,7 @@ enum FeatureFlag {
|
||||
|
||||
bool get isOn {
|
||||
if ([
|
||||
FeatureFlag.planBilling,
|
||||
// FeatureFlag.planBilling,
|
||||
// release this feature in version 0.6.1
|
||||
FeatureFlag.spaceDesign,
|
||||
// release this feature in version 0.5.9
|
||||
|
@ -90,7 +90,7 @@ class CompletionStream {
|
||||
if (event == "AI_RESPONSE_LIMIT") {
|
||||
onError(
|
||||
AIError(
|
||||
message: LocaleKeys.sideBar_aiResponseLitmit.tr(),
|
||||
message: LocaleKeys.sideBar_aiResponseLimit.tr(),
|
||||
code: AIErrorCode.aiResponseLimitExceeded,
|
||||
),
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -14,6 +14,7 @@ abstract class IUserBackendService {
|
||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
String? reason,
|
||||
);
|
||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||
String workspaceId,
|
||||
@ -21,6 +22,9 @@ abstract class IUserBackendService {
|
||||
);
|
||||
}
|
||||
|
||||
const _baseBetaUrl = 'https://beta.appflowy.com';
|
||||
const _baseProdUrl = 'https://appflowy.com';
|
||||
|
||||
class UserBackendService implements IUserBackendService {
|
||||
UserBackendService({required this.userId});
|
||||
|
||||
@ -255,19 +259,24 @@ class UserBackendService implements IUserBackendService {
|
||||
..recurringInterval = RecurringIntervalPB.Year
|
||||
..workspaceSubscriptionPlan = plan
|
||||
..successUrl =
|
||||
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success?plan=${plan.toRecognizable()}';
|
||||
'${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}';
|
||||
return UserEventSubscribeWorkspace(request).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
) {
|
||||
SubscriptionPlanPB plan, [
|
||||
String? reason,
|
||||
]) {
|
||||
final request = CancelWorkspaceSubscriptionPB()
|
||||
..workspaceId = workspaceId
|
||||
..plan = plan;
|
||||
|
||||
if (reason != null) {
|
||||
request.reason = reason;
|
||||
}
|
||||
|
||||
return UserEventCancelWorkspaceSubscription(request).send();
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ class SettingsBillingBloc
|
||||
(f) => Log.error(f.msg, f),
|
||||
);
|
||||
},
|
||||
cancelSubscription: (plan) async {
|
||||
cancelSubscription: (plan, reason) async {
|
||||
final s = state.mapOrNull(ready: (s) => s);
|
||||
if (s == null) {
|
||||
return;
|
||||
@ -124,7 +124,7 @@ class SettingsBillingBloc
|
||||
emit(s.copyWith(isLoading: true));
|
||||
|
||||
final result =
|
||||
await _userService.cancelSubscription(workspaceId, plan);
|
||||
await _userService.cancelSubscription(workspaceId, plan, reason);
|
||||
final successOrNull = result.fold(
|
||||
(_) => true,
|
||||
(f) {
|
||||
@ -276,8 +276,9 @@ class SettingsBillingEvent with _$SettingsBillingEvent {
|
||||
_AddSubscription;
|
||||
|
||||
const factory SettingsBillingEvent.cancelSubscription(
|
||||
SubscriptionPlanPB plan,
|
||||
) = _CancelSubscription;
|
||||
SubscriptionPlanPB plan, {
|
||||
@Default(null) String? reason,
|
||||
}) = _CancelSubscription;
|
||||
|
||||
const factory SettingsBillingEvent.paymentSuccessful({
|
||||
SubscriptionPlanPB? plan,
|
||||
|
@ -95,7 +95,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
cancelSubscription: () async {
|
||||
cancelSubscription: (reason) async {
|
||||
final newState = state
|
||||
.mapOrNull(ready: (state) => state)
|
||||
?.copyWith(downgradeProcessing: true);
|
||||
@ -106,6 +106,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
final result = await _userService.cancelSubscription(
|
||||
workspaceId,
|
||||
SubscriptionPlanPB.Pro,
|
||||
reason,
|
||||
);
|
||||
|
||||
final successOrNull = result.fold(
|
||||
@ -206,7 +207,9 @@ class SettingsPlanEvent with _$SettingsPlanEvent {
|
||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||
_AddSubscription;
|
||||
|
||||
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
||||
const factory SettingsPlanEvent.cancelSubscription({
|
||||
@Default(null) String? reason,
|
||||
}) = _CancelSubscription;
|
||||
|
||||
const factory SettingsPlanEvent.paymentSuccessful({
|
||||
@Default(null) SubscriptionPlanPB? plan,
|
||||
|
@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
||||
extension SubscriptionInfoHelpers on WorkspaceSubscriptionInfoPB {
|
||||
String get label => switch (plan) {
|
||||
WorkspacePlanPB.FreePlan =>
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
||||
@ -24,6 +24,14 @@ extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
||||
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
|
||||
_ => 'N/A',
|
||||
};
|
||||
|
||||
bool get isBillingPortalEnabled {
|
||||
if (plan != WorkspacePlanPB.FreePlan || addOns.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
extension AllSubscriptionLabels on SubscriptionPlanPB {
|
||||
|
@ -16,7 +16,7 @@ part 'sidebar_plan_bloc.freezed.dart';
|
||||
|
||||
class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
SidebarPlanBloc() : super(const SidebarPlanState()) {
|
||||
// After user pays for the subscription, the subscription success listenable will be triggered
|
||||
// 1. Listen to user subscription payment callback. After user client 'Open AppFlowy', this listenable will be triggered.
|
||||
final subscriptionListener = getIt<SubscriptionSuccessListenable>();
|
||||
subscriptionListener.addListener(() {
|
||||
final plan = subscriptionListener.subscribedPlan;
|
||||
@ -49,6 +49,7 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Listen to the storage notification
|
||||
_storageListener = StoreageNotificationListener(
|
||||
onError: (error) {
|
||||
if (!isClosed) {
|
||||
@ -57,6 +58,7 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Listen to specific error codes
|
||||
_globalErrorListener = GlobalErrorCodeNotifier.add(
|
||||
onError: (error) {
|
||||
if (!isClosed) {
|
||||
@ -92,11 +94,21 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
) async {
|
||||
await event.when(
|
||||
receiveError: (FlowyError error) async {
|
||||
if (error.code == ErrorCode.AIResponseLimitExceeded) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
tierIndicator: const SidebarToastTierIndicator.aiMaxiLimitHit(),
|
||||
),
|
||||
);
|
||||
} else if (error.code == ErrorCode.FileStorageLimitExceeded) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
tierIndicator: const SidebarToastTierIndicator.storageLimitHit(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Log.error("Unhandle Unexpected error: $error");
|
||||
}
|
||||
},
|
||||
init: (String workspaceId, UserProfilePB userProfile) {
|
||||
emit(
|
||||
|
@ -1,3 +1,7 @@
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
@ -7,14 +11,16 @@ import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SidebarFooter extends StatelessWidget {
|
||||
const SidebarFooter({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Row(
|
||||
return Column(
|
||||
children: [
|
||||
if (FeatureFlag.planBilling.isOn) const SidebarToast(),
|
||||
const Row(
|
||||
children: [
|
||||
Expanded(child: SidebarTrashButton()),
|
||||
// Enable it when the widget button is ready
|
||||
@ -24,14 +30,14 @@ class SidebarFooter extends StatelessWidget {
|
||||
// ),
|
||||
// Expanded(child: SidebarWidgetButton()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SidebarTrashButton extends StatelessWidget {
|
||||
const SidebarTrashButton({
|
||||
super.key,
|
||||
});
|
||||
const SidebarTrashButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
@ -6,88 +11,79 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SidebarToast extends StatefulWidget {
|
||||
class SidebarToast extends StatelessWidget {
|
||||
const SidebarToast({super.key});
|
||||
|
||||
@override
|
||||
State<SidebarToast> createState() => _SidebarToastState();
|
||||
}
|
||||
|
||||
class _SidebarToastState extends State<SidebarToast> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<SidebarPlanBloc, SidebarPlanState>(
|
||||
listener: (context, state) {
|
||||
listener: (_, state) {
|
||||
// Show a dialog when the user hits the storage limit, After user click ok, it will navigate to the plan page.
|
||||
// Even though the dislog is dissmissed, if the user triggers the storage limit again, the dialog will show again.
|
||||
state.tierIndicator.maybeWhen(
|
||||
storageLimitHit: () {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
storageLimitHit: () => WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _showStorageLimitDialog(context),
|
||||
debugLabel: 'Sidebar.showStorageLimit',
|
||||
),
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
orElse: () {
|
||||
// Do nothing
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return BlocBuilder<SidebarPlanBloc, SidebarPlanState>(
|
||||
builder: (context, state) {
|
||||
builder: (_, state) {
|
||||
return state.tierIndicator.when(
|
||||
storageLimitHit: () => Column(
|
||||
children: [
|
||||
const Divider(height: 0.6),
|
||||
PlanIndicator(
|
||||
planName: "Pro",
|
||||
loading: () => const SizedBox.shrink(),
|
||||
storageLimitHit: () => PlanIndicator(
|
||||
planName: SubscriptionPlanPB.Free.label,
|
||||
text: LocaleKeys.sideBar_upgradeToPro.tr(),
|
||||
onTap: () {
|
||||
_hanldeOnTap(context, SubscriptionPlanPB.Pro);
|
||||
},
|
||||
onTap: () => _hanldeOnTap(context, SubscriptionPlanPB.Pro),
|
||||
reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
aiMaxiLimitHit: () => Column(
|
||||
children: [
|
||||
const Divider(height: 0.6),
|
||||
PlanIndicator(
|
||||
planName: "AI Max",
|
||||
aiMaxiLimitHit: () => PlanIndicator(
|
||||
planName: SubscriptionPlanPB.AiMax.label,
|
||||
text: LocaleKeys.sideBar_upgradeToAIMax.tr(),
|
||||
onTap: () {
|
||||
_hanldeOnTap(context, SubscriptionPlanPB.AiMax);
|
||||
},
|
||||
reason: LocaleKeys.sideBar_aiResponseLitmitDialogTitle.tr(),
|
||||
onTap: () => _hanldeOnTap(context, SubscriptionPlanPB.AiMax),
|
||||
reason: LocaleKeys.sideBar_aiResponseLimitTitle.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showStorageLimitDialog(BuildContext context) {
|
||||
showDialog(
|
||||
void _showStorageLimitDialog(BuildContext context) => showConfirmDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (dialogContext) => _StorageLimitDialog(
|
||||
onOkPressed: () {
|
||||
title: LocaleKeys.sideBar_purchaseStorageSpace.tr(),
|
||||
description: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
||||
confirmLabel:
|
||||
LocaleKeys.settings_comparePlanDialog_actions_upgrade.tr(),
|
||||
onConfirm: () {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _hanldeOnTap(context, SubscriptionPlanPB.Pro),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
void _hanldeOnTap(BuildContext context, SubscriptionPlanPB plan) {
|
||||
final userProfile = context.read<SidebarPlanBloc>().state.userProfile;
|
||||
if (userProfile == null) {
|
||||
return Log.error(
|
||||
'UserProfile is null, this should NOT happen! Please file a bug report',
|
||||
);
|
||||
}
|
||||
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
if (userProfile != null) {
|
||||
final member = userWorkspaceBloc.state.currentWorkspaceMember;
|
||||
if (member == null) {
|
||||
return Log.error(
|
||||
"Member is null. It should not happen. If you see this error, it's a bug",
|
||||
);
|
||||
}
|
||||
|
||||
// Only if the user is the workspace owner will we navigate to the plan page.
|
||||
if (member.role.isOwner) {
|
||||
showSettingsDialog(
|
||||
context,
|
||||
userProfile,
|
||||
@ -95,36 +91,30 @@ class _SidebarToastState extends State<SidebarToast> {
|
||||
SettingsPage.plan,
|
||||
);
|
||||
} else {
|
||||
Log.error(
|
||||
"UserProfile is null. It should not happen. If you see this error, it's a bug.",
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final message = plan == SubscriptionPlanPB.AiMax
|
||||
? LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr()
|
||||
: LocaleKeys.sideBar_askOwnerToUpgradeToPro.tr();
|
||||
|
||||
void _hanldeOnTap(BuildContext context, SubscriptionPlanPB plan) {
|
||||
final userProfile = context.read<SidebarPlanBloc>().state.userProfile;
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
if (userProfile != null) {
|
||||
showSettingsDialog(
|
||||
context,
|
||||
userProfile,
|
||||
userWorkspaceBloc,
|
||||
SettingsPage.plan,
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (dialogContext) => _AskOwnerToChangePlan(
|
||||
message: message,
|
||||
onOkPressed: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlanIndicator extends StatelessWidget {
|
||||
class PlanIndicator extends StatefulWidget {
|
||||
const PlanIndicator({
|
||||
super.key,
|
||||
required this.planName,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
required this.reason,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String planName;
|
||||
@ -132,62 +122,150 @@ class PlanIndicator extends StatelessWidget {
|
||||
final String text;
|
||||
final Function() onTap;
|
||||
|
||||
final textColor = const Color(0xFFE8E2EE);
|
||||
final secondaryColor = const Color(0xFF653E8C);
|
||||
@override
|
||||
State<PlanIndicator> createState() => _PlanIndicatorState();
|
||||
}
|
||||
|
||||
class _PlanIndicatorState extends State<PlanIndicator> {
|
||||
final popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
const textGradient = LinearGradient(
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],
|
||||
stops: [0.1545, 0.8225],
|
||||
);
|
||||
|
||||
final backgroundGradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFF8032FF).withOpacity(.1),
|
||||
const Color(0xFFEF35FF).withOpacity(.1),
|
||||
],
|
||||
);
|
||||
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.rightWithBottomAligned,
|
||||
offset: const Offset(10, -12),
|
||||
popupBuilder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
text: FlowyText(
|
||||
text,
|
||||
color: textColor,
|
||||
fontSize: 12,
|
||||
FlowyText(
|
||||
widget.text,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
radius: BorderRadius.zero,
|
||||
leftIconSize: const Size(40, 20),
|
||||
leftIcon: Badge(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
backgroundColor: secondaryColor,
|
||||
label: FlowyText.semibold(
|
||||
planName,
|
||||
fontSize: 12,
|
||||
color: textColor,
|
||||
const VSpace(12),
|
||||
Opacity(
|
||||
opacity: 0.7,
|
||||
child: FlowyText.regular(
|
||||
widget.reason,
|
||||
maxLines: null,
|
||||
lineHeight: 1.3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
const VSpace(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
popoverController.close();
|
||||
widget.onTap();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 6),
|
||||
child: Opacity(
|
||||
opacity: 0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Center(
|
||||
child: FlowyText(
|
||||
reason,
|
||||
textAlign: TextAlign.start,
|
||||
color: textColor,
|
||||
fontSize: 8,
|
||||
maxLines: 10,
|
||||
LocaleKeys
|
||||
.settings_comparePlanDialog_actions_upgrade
|
||||
.tr(),
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
strutStyle: const StrutStyle(
|
||||
forceStrutHeight: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
gradient: backgroundGradient,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.upgrade_storage_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(6),
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => textGradient.createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: FlowyText(
|
||||
widget.text,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StorageLimitDialog extends StatelessWidget {
|
||||
const _StorageLimitDialog({
|
||||
class _AskOwnerToChangePlan extends StatelessWidget {
|
||||
const _AskOwnerToChangePlan({
|
||||
required this.message,
|
||||
required this.onOkPressed,
|
||||
});
|
||||
final String message;
|
||||
final VoidCallback onOkPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
||||
okTitle: LocaleKeys.sideBar_purchaseStorageSpace.tr(),
|
||||
message: message,
|
||||
okTitle: LocaleKeys.button_ok.tr(),
|
||||
onOkPressed: onOkPressed,
|
||||
titleUpperCase: false,
|
||||
);
|
||||
|
@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/home/af_focus_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/blank/blank.dart';
|
||||
@ -36,7 +38,6 @@ import 'package:easy_localization/easy_localization.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
Loading? _duplicateSpaceLoading;
|
||||
@ -358,9 +359,6 @@ class _SidebarState extends State<_Sidebar> {
|
||||
child: const SidebarFooter(),
|
||||
),
|
||||
const VSpace(14),
|
||||
|
||||
// toast
|
||||
// const SidebarToast(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,10 +1,11 @@
|
||||
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/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SpaceMigration extends StatefulWidget {
|
||||
@ -70,14 +71,8 @@ class _SpaceMigrationState extends State<SpaceMigration> {
|
||||
const linearGradient = LinearGradient(
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFF8032FF),
|
||||
Color(0xFFEF35FF),
|
||||
],
|
||||
stops: [
|
||||
0.1545,
|
||||
0.8225,
|
||||
],
|
||||
colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],
|
||||
stops: [0.1545, 0.8225],
|
||||
);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
|
@ -91,7 +91,7 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
|
||||
},
|
||||
ready: (state) {
|
||||
final billingPortalEnabled =
|
||||
state.subscriptionInfo.plan != WorkspacePlanPB.FreePlan;
|
||||
state.subscriptionInfo.isBillingPortalEnabled;
|
||||
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||
@ -327,14 +327,9 @@ class _AITileState extends State<_AITile> {
|
||||
: LocaleKeys.settings_billingPage_addons_addLabel.tr(),
|
||||
fontWeight: FontWeight.w500,
|
||||
minWidth: _buttonsMinWidth,
|
||||
onPressed: () {
|
||||
if (widget.subscriptionInfo != null && isCanceled) {
|
||||
// Show customer portal to renew
|
||||
context
|
||||
.read<SettingsBillingBloc>()
|
||||
.add(const SettingsBillingEvent.openCustomerPortal());
|
||||
} else if (widget.subscriptionInfo != null) {
|
||||
showConfirmDialog(
|
||||
onPressed: () async {
|
||||
if (widget.subscriptionInfo != null) {
|
||||
await showConfirmDialog(
|
||||
context: context,
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
title: LocaleKeys.settings_billingPage_addons_removeDialog_title
|
||||
@ -343,11 +338,9 @@ class _AITileState extends State<_AITile> {
|
||||
.settings_billingPage_addons_removeDialog_description
|
||||
.tr(namedArgs: {"plan": widget.plan.label.tr()}),
|
||||
confirmLabel: LocaleKeys.button_confirm.tr(),
|
||||
onConfirm: () {
|
||||
context.read<SettingsBillingBloc>().add(
|
||||
SettingsBillingEvent.cancelSubscription(widget.plan),
|
||||
);
|
||||
},
|
||||
onConfirm: () => context
|
||||
.read<SettingsBillingBloc>()
|
||||
.add(SettingsBillingEvent.cancelSubscription(widget.plan)),
|
||||
);
|
||||
} else {
|
||||
// Add the addon
|
||||
|
@ -5,6 +5,7 @@ import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -141,7 +142,7 @@ class _SettingsPlanComparisonDialogState
|
||||
children: [
|
||||
const VSpace(30),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
height: 116,
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys
|
||||
.settings_comparePlanDialog_planFeatures
|
||||
@ -153,7 +154,7 @@ class _SettingsPlanComparisonDialogState
|
||||
: const Color(0xFFE8E0FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 96),
|
||||
const SizedBox(height: 116),
|
||||
const SizedBox(height: 56),
|
||||
..._planLabels.map(
|
||||
(e) => _ComparisonCell(
|
||||
@ -184,17 +185,9 @@ class _SettingsPlanComparisonDialogState
|
||||
cells: _freeLabels,
|
||||
isCurrent:
|
||||
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
||||
canDowngrade:
|
||||
currentInfo.plan != WorkspacePlanPB.FreePlan,
|
||||
currentCanceled: currentInfo.isCanceled ||
|
||||
(context
|
||||
.watch<SettingsPlanBloc>()
|
||||
.state
|
||||
.mapOrNull(
|
||||
loading: (_) => true,
|
||||
ready: (s) => s.downgradeProcessing,
|
||||
) ??
|
||||
false),
|
||||
buttonType: WorkspacePlanPB.FreePlan.buttonTypeFor(
|
||||
currentInfo.plan,
|
||||
),
|
||||
onSelected: () async {
|
||||
if (currentInfo.plan ==
|
||||
WorkspacePlanPB.FreePlan ||
|
||||
@ -202,6 +195,12 @@ class _SettingsPlanComparisonDialogState
|
||||
return;
|
||||
}
|
||||
|
||||
final reason =
|
||||
await showCancelSurveyDialog(context);
|
||||
if (reason == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys
|
||||
@ -216,8 +215,9 @@ class _SettingsPlanComparisonDialogState
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
onConfirm: () =>
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent
|
||||
.cancelSubscription(),
|
||||
SettingsPlanEvent.cancelSubscription(
|
||||
reason: reason,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -242,9 +242,9 @@ class _SettingsPlanComparisonDialogState
|
||||
cells: _proLabels,
|
||||
isCurrent:
|
||||
currentInfo.plan == WorkspacePlanPB.ProPlan,
|
||||
canUpgrade:
|
||||
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
||||
currentCanceled: currentInfo.isCanceled,
|
||||
buttonType: WorkspacePlanPB.ProPlan.buttonTypeFor(
|
||||
currentInfo.plan,
|
||||
),
|
||||
onSelected: () =>
|
||||
context.read<SettingsPlanBloc>().add(
|
||||
const SettingsPlanEvent.addSubscription(
|
||||
@ -266,6 +266,35 @@ class _SettingsPlanComparisonDialogState
|
||||
}
|
||||
}
|
||||
|
||||
enum _PlanButtonType {
|
||||
none,
|
||||
upgrade,
|
||||
downgrade;
|
||||
|
||||
bool get isDowngrade => this == downgrade;
|
||||
bool get isUpgrade => this == upgrade;
|
||||
}
|
||||
|
||||
extension _ButtonTypeFrom on WorkspacePlanPB {
|
||||
/// Returns the button type for the given plan, taking the
|
||||
/// current plan as [other].
|
||||
///
|
||||
_PlanButtonType buttonTypeFor(WorkspacePlanPB other) {
|
||||
/// Current plan, no action
|
||||
if (this == other) {
|
||||
return _PlanButtonType.none;
|
||||
}
|
||||
|
||||
// Free plan, can downgrade if not on the free plan
|
||||
if (this == WorkspacePlanPB.FreePlan && other != WorkspacePlanPB.FreePlan) {
|
||||
return _PlanButtonType.downgrade;
|
||||
}
|
||||
|
||||
// Else we can assume it's an upgrade
|
||||
return _PlanButtonType.upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanTable extends StatelessWidget {
|
||||
const _PlanTable({
|
||||
required this.title,
|
||||
@ -275,9 +304,7 @@ class _PlanTable extends StatelessWidget {
|
||||
required this.cells,
|
||||
required this.isCurrent,
|
||||
required this.onSelected,
|
||||
this.canUpgrade = false,
|
||||
this.canDowngrade = false,
|
||||
this.currentCanceled = false,
|
||||
this.buttonType = _PlanButtonType.none,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@ -288,13 +315,11 @@ class _PlanTable extends StatelessWidget {
|
||||
final List<_CellItem> cells;
|
||||
final bool isCurrent;
|
||||
final VoidCallback onSelected;
|
||||
final bool canUpgrade;
|
||||
final bool canDowngrade;
|
||||
final bool currentCanceled;
|
||||
final _PlanButtonType buttonType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final highlightPlan = !isCurrent && !canDowngrade && canUpgrade;
|
||||
final highlightPlan = !isCurrent && buttonType == _PlanButtonType.upgrade;
|
||||
final isLM = Theme.of(context).isLightMode;
|
||||
|
||||
return Container(
|
||||
@ -336,37 +361,29 @@ class _PlanTable extends StatelessWidget {
|
||||
title: price,
|
||||
description: priceInfo,
|
||||
isPrimary: !highlightPlan,
|
||||
height: 96,
|
||||
),
|
||||
if (canUpgrade || canDowngrade) ...[
|
||||
if (buttonType == _PlanButtonType.none) ...[
|
||||
const SizedBox(height: 56),
|
||||
] else ...[
|
||||
Opacity(
|
||||
opacity: canDowngrade && currentCanceled ? 0.5 : 1,
|
||||
opacity: 1,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 12 + (canUpgrade && !canDowngrade ? 12 : 0),
|
||||
left: 12 + (buttonType.isUpgrade ? 12 : 0),
|
||||
),
|
||||
child: _ActionButton(
|
||||
label: canUpgrade && !canDowngrade
|
||||
label: buttonType.isUpgrade
|
||||
? 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,
|
||||
onPressed: onSelected,
|
||||
isUpgrade: buttonType.isUpgrade,
|
||||
useGradientBorder: buttonType.isUpgrade,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 56),
|
||||
],
|
||||
...cells.map(
|
||||
(cell) => _ComparisonCell(
|
||||
@ -467,14 +484,12 @@ 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 String? tooltip;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isUpgrade;
|
||||
final bool useGradientBorder;
|
||||
@ -487,9 +502,7 @@ class _ActionButton extends StatelessWidget {
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyTooltip(
|
||||
message: tooltip,
|
||||
child: GestureDetector(
|
||||
GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: MouseRegion(
|
||||
cursor: onPressed != null
|
||||
@ -514,7 +527,6 @@ class _ActionButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -538,10 +550,7 @@ class _ActionButton extends StatelessWidget {
|
||||
shaderCallback: (bounds) => const LinearGradient(
|
||||
transform: GradientRotation(-1.55),
|
||||
stops: [0.4, 1],
|
||||
colors: [
|
||||
Color(0xFF251D37),
|
||||
Color(0xFF7547C0),
|
||||
],
|
||||
colors: [Color(0xFF251D37), Color(0xFF7547C0)],
|
||||
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
||||
child: child,
|
||||
);
|
||||
@ -579,19 +588,17 @@ class _Heading extends StatelessWidget {
|
||||
required this.title,
|
||||
this.description,
|
||||
this.isPrimary = true,
|
||||
this.height = 100,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? description;
|
||||
final bool isPrimary;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 185,
|
||||
height: height,
|
||||
height: 116,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
|
||||
child: Column(
|
||||
@ -615,12 +622,14 @@ class _Heading extends StatelessWidget {
|
||||
),
|
||||
if (description != null && description!.isNotEmpty) ...[
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
description!,
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
maxLines: 5,
|
||||
lineHeight: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
@ -232,7 +232,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
||||
const VSpace(8),
|
||||
FlowyText.regular(
|
||||
widget.subscriptionInfo.info,
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
maxLines: 3,
|
||||
),
|
||||
|
@ -0,0 +1,431 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:collection/collection.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';
|
||||
|
||||
Future<String?> showCancelSurveyDialog(BuildContext context) {
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
builder: (_) => const _Survey(),
|
||||
);
|
||||
}
|
||||
|
||||
class _Survey extends StatefulWidget {
|
||||
const _Survey();
|
||||
|
||||
@override
|
||||
State<_Survey> createState() => _SurveyState();
|
||||
}
|
||||
|
||||
class _SurveyState extends State<_Survey> {
|
||||
final PageController pageController = PageController();
|
||||
final Map<String, String> answers = {};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 674,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Survey title
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_cancelSurveyDialog_title.tr(),
|
||||
fontSize: 22.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: const FlowySvg(FlowySvgs.upgrade_close_s),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12),
|
||||
// Survey explanation
|
||||
FlowyText(
|
||||
LocaleKeys.settings_cancelSurveyDialog_description.tr(),
|
||||
maxLines: 3,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Divider(),
|
||||
const VSpace(8),
|
||||
// Question "sheet"
|
||||
SizedBox(
|
||||
height: 400,
|
||||
width: 650,
|
||||
child: PageView.builder(
|
||||
controller: pageController,
|
||||
itemCount: _questionsAndAnswers.length,
|
||||
itemBuilder: (context, index) => _QAPage(
|
||||
qa: _questionsAndAnswers[index],
|
||||
isFirstQuestion: index == 0,
|
||||
isFinalQuestion:
|
||||
index == _questionsAndAnswers.length - 1,
|
||||
selectedAnswer:
|
||||
answers[_questionsAndAnswers[index].question],
|
||||
onPrevious: () {
|
||||
if (index > 0) {
|
||||
pageController.animateToPage(
|
||||
index - 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
onAnswerChanged: (answer) {
|
||||
answers[_questionsAndAnswers[index].question] =
|
||||
answer;
|
||||
},
|
||||
onAnswerSelected: (answer) {
|
||||
answers[_questionsAndAnswers[index].question] =
|
||||
answer;
|
||||
|
||||
if (index == _questionsAndAnswers.length - 1) {
|
||||
Navigator.of(context).pop(jsonEncode(answers));
|
||||
} else {
|
||||
pageController.animateToPage(
|
||||
index + 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QAPage extends StatefulWidget {
|
||||
const _QAPage({
|
||||
required this.qa,
|
||||
required this.onAnswerSelected,
|
||||
required this.onAnswerChanged,
|
||||
required this.onPrevious,
|
||||
this.selectedAnswer,
|
||||
this.isFirstQuestion = false,
|
||||
this.isFinalQuestion = false,
|
||||
});
|
||||
|
||||
final _QA qa;
|
||||
final String? selectedAnswer;
|
||||
|
||||
/// Called when "Next" is pressed
|
||||
///
|
||||
final Function(String) onAnswerSelected;
|
||||
|
||||
/// Called whenever an answer is selected or changed
|
||||
///
|
||||
final Function(String) onAnswerChanged;
|
||||
final VoidCallback onPrevious;
|
||||
final bool isFirstQuestion;
|
||||
final bool isFinalQuestion;
|
||||
|
||||
@override
|
||||
State<_QAPage> createState() => _QAPageState();
|
||||
}
|
||||
|
||||
class _QAPageState extends State<_QAPage> {
|
||||
final otherController = TextEditingController();
|
||||
|
||||
int _selectedIndex = -1;
|
||||
String? answer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.selectedAnswer != null) {
|
||||
answer = widget.selectedAnswer;
|
||||
_selectedIndex = widget.qa.answers.indexOf(widget.selectedAnswer!);
|
||||
if (_selectedIndex == -1) {
|
||||
// We assume the last question is "Other"
|
||||
_selectedIndex = widget.qa.answers.length - 1;
|
||||
otherController.text = widget.selectedAnswer!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText(
|
||||
widget.qa.question,
|
||||
fontSize: 16.0,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
const VSpace(18),
|
||||
SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(6),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.qa.answers
|
||||
.mapIndexed(
|
||||
(index, option) => _AnswerOption(
|
||||
prefix: _indexToLetter(index),
|
||||
option: option,
|
||||
isSelected: _selectedIndex == index,
|
||||
onTap: () => setState(() {
|
||||
_selectedIndex = index;
|
||||
if (_selectedIndex == widget.qa.answers.length - 1 &&
|
||||
widget.qa.lastIsOther) {
|
||||
answer = otherController.text;
|
||||
} else {
|
||||
answer = option;
|
||||
}
|
||||
widget.onAnswerChanged(option);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (widget.qa.lastIsOther &&
|
||||
_selectedIndex == widget.qa.answers.length - 1) ...[
|
||||
const VSpace(8),
|
||||
FlowyTextField(
|
||||
controller: otherController,
|
||||
hintText: LocaleKeys.settings_cancelSurveyDialog_otherHint.tr(),
|
||||
onChanged: (value) => setState(() {
|
||||
answer = value;
|
||||
widget.onAnswerChanged(value);
|
||||
}),
|
||||
),
|
||||
],
|
||||
const VSpace(20),
|
||||
Row(
|
||||
children: [
|
||||
if (!widget.isFirstQuestion) ...[
|
||||
DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(color: Color(0x1E14171B)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 9.0,
|
||||
),
|
||||
text: FlowyText.regular(LocaleKeys.button_previous.tr()),
|
||||
onTap: widget.onPrevious,
|
||||
),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
],
|
||||
DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
|
||||
radius: BorderRadius.circular(8),
|
||||
text: FlowyText.regular(
|
||||
widget.isFinalQuestion
|
||||
? LocaleKeys.button_submit.tr()
|
||||
: LocaleKeys.button_next.tr(),
|
||||
color: Colors.white,
|
||||
),
|
||||
disable: !canProceed(),
|
||||
onTap: canProceed()
|
||||
? () => widget.onAnswerSelected(
|
||||
answer ?? widget.qa.answers[_selectedIndex],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool canProceed() {
|
||||
if (_selectedIndex == widget.qa.answers.length - 1 &&
|
||||
widget.qa.lastIsOther) {
|
||||
return answer != null &&
|
||||
answer!.isNotEmpty &&
|
||||
answer != LocaleKeys.settings_cancelSurveyDialog_commonOther.tr();
|
||||
}
|
||||
|
||||
return _selectedIndex != -1;
|
||||
}
|
||||
}
|
||||
|
||||
class _AnswerOption extends StatelessWidget {
|
||||
const _AnswerOption({
|
||||
required this.prefix,
|
||||
required this.option,
|
||||
required this.onTap,
|
||||
this.isSelected = false,
|
||||
});
|
||||
|
||||
final String prefix;
|
||||
final String option;
|
||||
final VoidCallback onTap;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: Corners.s8Border,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(2),
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).dividerColor,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Center(
|
||||
child: FlowyText(
|
||||
prefix,
|
||||
color: isSelected ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
FlowyText(
|
||||
option,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16.0,
|
||||
color: AFThemeExtension.of(context).strongText,
|
||||
),
|
||||
const HSpace(6),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _questionsAndAnswers = [
|
||||
_QA(
|
||||
question: LocaleKeys.settings_cancelSurveyDialog_questionOne_question.tr(),
|
||||
answers: [
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionOne_answerOne.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionOne_answerTwo.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionOne_answerThree.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFour.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFive.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(),
|
||||
],
|
||||
lastIsOther: true,
|
||||
),
|
||||
_QA(
|
||||
question: LocaleKeys.settings_cancelSurveyDialog_questionTwo_question.tr(),
|
||||
answers: [
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerOne.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerTwo.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerThree.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFour.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFive.tr(),
|
||||
],
|
||||
),
|
||||
_QA(
|
||||
question:
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionThree_question.tr(),
|
||||
answers: [
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionThree_answerOne.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionThree_answerTwo.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionThree_answerThree.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionThree_answerFour.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(),
|
||||
],
|
||||
lastIsOther: true,
|
||||
),
|
||||
_QA(
|
||||
question: LocaleKeys.settings_cancelSurveyDialog_questionFour_question.tr(),
|
||||
answers: [
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionFour_answerOne.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionFour_answerTwo.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionFour_answerThree.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFour.tr(),
|
||||
LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFive.tr(),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
class _QA {
|
||||
const _QA({
|
||||
required this.question,
|
||||
required this.answers,
|
||||
this.lastIsOther = false,
|
||||
});
|
||||
|
||||
final String question;
|
||||
final List<String> answers;
|
||||
final bool lastIsOther;
|
||||
}
|
||||
|
||||
/// Returns the letter corresponding to the index.
|
||||
///
|
||||
/// Eg. 0 -> A, 1 -> B, 2 -> C, ..., and so forth.
|
||||
///
|
||||
String _indexToLetter(int index) {
|
||||
return String.fromCharCode(65 + index);
|
||||
}
|
@ -72,8 +72,6 @@ class WorkspaceMembersPage extends StatelessWidget {
|
||||
final actionResult = state.actionResult!.result;
|
||||
final actionType = state.actionResult!.actionType;
|
||||
|
||||
debugPrint("Plan: ${state.subscriptionInfo?.plan}");
|
||||
|
||||
if (actionType == WorkspaceMemberActionType.invite &&
|
||||
actionResult.isFailure) {
|
||||
final error = actionResult.getFailure().code;
|
||||
|
@ -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';
|
||||
@ -111,26 +112,26 @@ class SettingsMenu extends StatelessWidget {
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
// if (FeatureFlag.planBilling.isOn &&
|
||||
// userProfile.authenticator ==
|
||||
// AuthenticatorPB.AppFlowyCloud &&
|
||||
// member != null &&
|
||||
// member!.role.isOwner) ...[
|
||||
// SettingsMenuElement(
|
||||
// page: SettingsPage.plan,
|
||||
// selectedPage: currentPage,
|
||||
// label: LocaleKeys.settings_planPage_menuLabel.tr(),
|
||||
// icon: const FlowySvg(FlowySvgs.settings_plan_m),
|
||||
// changeSelectedPage: changeSelectedPage,
|
||||
// ),
|
||||
// SettingsMenuElement(
|
||||
// page: SettingsPage.billing,
|
||||
// selectedPage: currentPage,
|
||||
// label: LocaleKeys.settings_billingPage_menuLabel.tr(),
|
||||
// icon: const FlowySvg(FlowySvgs.settings_billing_m),
|
||||
// changeSelectedPage: changeSelectedPage,
|
||||
// ),
|
||||
// ],
|
||||
if (FeatureFlag.planBilling.isOn &&
|
||||
userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud &&
|
||||
member != null &&
|
||||
member!.role.isOwner) ...[
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.plan,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_planPage_menuLabel.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.settings_plan_m),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.billing,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_billingPage_menuLabel.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.settings_billing_m),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
if (kDebugMode)
|
||||
SettingsMenuElement(
|
||||
// no need to translate this page
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@ -52,6 +53,8 @@ class StackTraceError {
|
||||
|
||||
typedef void ErrorListener();
|
||||
|
||||
/// Receive error when Rust backend send error message back to the flutter frontend
|
||||
///
|
||||
class GlobalErrorCodeNotifier extends ChangeNotifier {
|
||||
// Static instance with lazy initialization
|
||||
static final GlobalErrorCodeNotifier _instance =
|
||||
@ -107,3 +110,10 @@ class GlobalErrorCodeNotifier extends ChangeNotifier {
|
||||
_instance.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
extension FlowyErrorExtension on FlowyError {
|
||||
bool get isAIResponseLimitExceeded =>
|
||||
code == ErrorCode.AIResponseLimitExceeded;
|
||||
|
||||
bool get isStorageLimitExceeded => code == ErrorCode.FileStorageLimitExceeded;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||
@ -168,6 +169,37 @@ class FlowyTextButton extends StatelessWidget {
|
||||
this.borderColor,
|
||||
});
|
||||
|
||||
factory FlowyTextButton.primary({
|
||||
required BuildContext context,
|
||||
required String text,
|
||||
VoidCallback? onPressed,
|
||||
}) =>
|
||||
FlowyTextButton(
|
||||
text,
|
||||
constraints: const BoxConstraints(minHeight: 32),
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: const Color(0xFF005483),
|
||||
fontColor: AFThemeExtension.of(context).strongText,
|
||||
fontHoverColor: Colors.white,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
|
||||
factory FlowyTextButton.secondary({
|
||||
required BuildContext context,
|
||||
required String text,
|
||||
VoidCallback? onPressed,
|
||||
}) =>
|
||||
FlowyTextButton(
|
||||
text,
|
||||
constraints: const BoxConstraints(minHeight: 32),
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: Theme.of(context).colorScheme.primary,
|
||||
fontColor: Theme.of(context).colorScheme.primary,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
fontHoverColor: Colors.white,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
|
||||
final String text;
|
||||
final FontWeight? fontWeight;
|
||||
final Color? fontColor;
|
||||
|
15
frontend/resources/flowy_icons/16x/upgrade_storage.svg
Normal file
15
frontend/resources/flowy_icons/16x/upgrade_storage.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="#E8EAED"/>
|
||||
<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="url(#paint0_linear_3646_112419)"/>
|
||||
<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="url(#paint1_linear_3646_112419)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3646_112419" x1="10" y1="3" x2="10" y2="16" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8132FF" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#8132FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3646_112419" x1="7.54546" y1="14.8182" x2="15.0845" y2="11.9942" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8032FF"/>
|
||||
<stop offset="1" stop-color="#EF35FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 973 B |
@ -283,11 +283,14 @@
|
||||
"favoriteSpace": "Favorites",
|
||||
"RecentSpace": "Recent",
|
||||
"Spaces": "Spaces",
|
||||
"upgradeToPro": "Upgrade to Pro Plan",
|
||||
"upgradeToPro": "Upgrade to Pro",
|
||||
"upgradeToAIMax": "Unlock unlimited AI",
|
||||
"storageLimitDialogTitle": "You are running out of storage space. Upgrade to Pro Plan to get more storage",
|
||||
"aiResponseLitmitDialogTitle": "You are running out of AI responses. Upgrade to Pro Plan or AI Max to get more AI responses",
|
||||
"aiResponseLitmit": "You are running out of AI responses. Go to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses",
|
||||
"storageLimitDialogTitle": "You have run out of free storage. Upgrade to unlock unlimited storage",
|
||||
"aiResponseLimitTitle": "You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses",
|
||||
"aiResponseLimitDialogTitle": "AI Responses limit reached",
|
||||
"aiResponseLimit": "You have run out of free AI responses.\n\nGo to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses",
|
||||
"askOwnerToUpgradeToPro": "Your workspace is running out of free storage. Please ask your workspace owner to upgrade to the Pro Plan",
|
||||
"askOwnerToUpgradeToAIMax": "Your workspace is running out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons",
|
||||
"purchaseStorageSpace": "Purchase Storage Space",
|
||||
"purchaseAIResponse": "Purchase ",
|
||||
"upgradeToAILocal": "AI offline on your device"
|
||||
@ -350,7 +353,10 @@
|
||||
"signInDiscord": "Continue with Discord",
|
||||
"more": "More",
|
||||
"create": "Create",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Welcome!",
|
||||
@ -638,7 +644,7 @@
|
||||
"menuLabel": "AI Settings",
|
||||
"keys": {
|
||||
"enableAISearchTitle": "AI Search",
|
||||
"aiSettingsDescription": "Select or configure Ai models used on @:appName. For best performance we recommend using the default model options",
|
||||
"aiSettingsDescription": "Select or configure AI models used on @:appName. For best performance we recommend using the default model options",
|
||||
"loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up",
|
||||
"llmModel": "Language Model",
|
||||
"llmModelType": "Language Model Type",
|
||||
@ -704,14 +710,14 @@
|
||||
"title": "AI Max",
|
||||
"description": "Unlock unlimited AI",
|
||||
"price": "{}",
|
||||
"priceInfo": "/user per month",
|
||||
"priceInfo": "per user per month",
|
||||
"billingInfo": "billed annually or {} billed monthly"
|
||||
},
|
||||
"aiOnDevice": {
|
||||
"title": "AI On-device",
|
||||
"description": "AI offline on your device",
|
||||
"price": "{}",
|
||||
"priceInfo": "/user per month",
|
||||
"priceInfo": "per user per month",
|
||||
"billingInfo": "billed annually or {} billed monthly"
|
||||
}
|
||||
},
|
||||
@ -776,7 +782,6 @@
|
||||
"actions": {
|
||||
"upgrade": "Upgrade",
|
||||
"downgrade": "Downgrade",
|
||||
"downgradeDisabledTooltip": "You will automatically downgrade at the end of the billing cycle",
|
||||
"current": "Current"
|
||||
},
|
||||
"freePlan": {
|
||||
@ -789,7 +794,7 @@
|
||||
"title": "Pro",
|
||||
"description": "For small teams to manage projects and team knowledge",
|
||||
"price": "{}",
|
||||
"priceInfo": "/user per month billed annually\n\n{} billed monthly"
|
||||
"priceInfo": "per user per month \nbilled annually\n\n{} billed monthly"
|
||||
},
|
||||
"planLabels": {
|
||||
"itemOne": "Workspaces",
|
||||
@ -830,6 +835,43 @@
|
||||
"downgradeLabel": "Downgrade plan"
|
||||
}
|
||||
},
|
||||
"cancelSurveyDialog": {
|
||||
"title": "Sorry to see you go",
|
||||
"description": "We're sorry to see you go. We'd love to hear your feedback to help us improve @:appName. Please take a moment to answer a few questions.",
|
||||
"commonOther": "Other",
|
||||
"otherHint": "Write your answer here",
|
||||
"questionOne": {
|
||||
"question": "What prompted you to cancel your AppFlowy Pro subscription?",
|
||||
"answerOne": "Cost too high",
|
||||
"answerTwo": "Features did not meet expectations",
|
||||
"answerThree": "Found a better alternative",
|
||||
"answerFour": "Did not use it enough to justify the expense",
|
||||
"answerFive": "Service issue or technical difficulties"
|
||||
},
|
||||
"questionTwo": {
|
||||
"question": "How likely are you to consider re-subscribing to AppFlowy Pro in the future?",
|
||||
"answerOne": "Very likely",
|
||||
"answerTwo": "Somewhat likely",
|
||||
"answerThree": "Not sure",
|
||||
"answerFour": "Unlikely",
|
||||
"answerFive": "Very unlikely"
|
||||
},
|
||||
"questionThree": {
|
||||
"question": "Which Pro feature did you value the most during your subscription?",
|
||||
"answerOne": "Multi-user collaboration",
|
||||
"answerTwo": "Longer time version history",
|
||||
"answerThree": "Unlimited AI responses",
|
||||
"answerFour": "Access to local AI models"
|
||||
},
|
||||
"questionFour": {
|
||||
"question": "How would you describe your overall experience with AppFlowy?",
|
||||
"answerOne": "Great",
|
||||
"answerTwo": "Good",
|
||||
"answerThree": "Average",
|
||||
"answerFour": "Below average",
|
||||
"answerFive": "Unsatisfied"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"reset": "Reset"
|
||||
},
|
||||
@ -1441,7 +1483,7 @@
|
||||
"image": {
|
||||
"copiedToPasteBoard": "The image link has been copied to the clipboard",
|
||||
"addAnImage": "Add an image",
|
||||
"imageUploadFailed": "Image upload failed",
|
||||
"imageUploadFailed": "Upload failed",
|
||||
"errorCode": "Error code"
|
||||
},
|
||||
"math": {
|
||||
@ -2130,7 +2172,7 @@
|
||||
"zero": "Publish {} selected view",
|
||||
"one": "Publish {} selected views",
|
||||
"many": "Publish {} selected views",
|
||||
"other":"Publish {} selected views"
|
||||
"other": "Publish {} selected views"
|
||||
},
|
||||
"mustSelectPrimaryDatabase": "The primary view must be selected",
|
||||
"noDatabaseSelected": "No database selected, please select at least one database.",
|
||||
|
@ -148,7 +148,12 @@ impl Chat {
|
||||
},
|
||||
Err(err) => {
|
||||
error!("[Chat] failed to stream answer: {}", err);
|
||||
if err.is_ai_response_limit_exceeded() {
|
||||
let _ = text_sink.send("AI_RESPONSE_LIMIT".to_string()).await;
|
||||
} else {
|
||||
let _ = text_sink.send(format!("error:{}", err)).await;
|
||||
}
|
||||
|
||||
let pb = ChatMessageErrorPB {
|
||||
chat_id: chat_id.clone(),
|
||||
error_message: err.to_string(),
|
||||
|
Loading…
Reference in New Issue
Block a user