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,
|
required this.questionId,
|
||||||
}) : super(ChatAIMessageState.initial(message)) {
|
}) : super(ChatAIMessageState.initial(message)) {
|
||||||
if (state.stream != null) {
|
if (state.stream != null) {
|
||||||
_subscription = state.stream!.listen((text) {
|
_subscription = state.stream!.listen(
|
||||||
if (isClosed) {
|
onData: (text) {
|
||||||
return;
|
if (!isClosed) {
|
||||||
|
add(ChatAIMessageEvent.updateText(text));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (text.startsWith("data:")) {
|
onError: (error) {
|
||||||
add(ChatAIMessageEvent.newText(text.substring(5)));
|
if (!isClosed) {
|
||||||
} else if (text.startsWith("error:")) {
|
add(ChatAIMessageEvent.receiveError(error.toString()));
|
||||||
add(ChatAIMessageEvent.receiveError(text.substring(5)));
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
onAIResponseLimit: () {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (state.stream!.error != null) {
|
if (state.stream!.error != null) {
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
@ -42,11 +48,16 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
initial: () async {},
|
initial: () async {},
|
||||||
newText: (newText) {
|
updateText: (newText) {
|
||||||
emit(state.copyWith(text: state.text + newText, error: null));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
text: newText,
|
||||||
|
messageState: const MessageState.ready(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
receiveError: (error) {
|
receiveError: (error) {
|
||||||
emit(state.copyWith(error: error));
|
emit(state.copyWith(messageState: MessageState.onError(error)));
|
||||||
},
|
},
|
||||||
retry: () {
|
retry: () {
|
||||||
if (questionId is! Int64) {
|
if (questionId is! Int64) {
|
||||||
@ -55,8 +66,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
}
|
}
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
retryState: const LoadingState.loading(),
|
messageState: const MessageState.loading(),
|
||||||
error: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,8 +92,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
text: text,
|
text: text,
|
||||||
error: null,
|
messageState: const MessageState.ready(),
|
||||||
retryState: const LoadingState.finish(),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAIResponseLimit: () {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
messageState: const MessageState.onAIResponseLimit(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -98,7 +114,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<AnswerStreamElement>? _subscription;
|
StreamSubscription<String>? _subscription;
|
||||||
final String chatId;
|
final String chatId;
|
||||||
final Int64? questionId;
|
final Int64? questionId;
|
||||||
}
|
}
|
||||||
@ -106,26 +122,34 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
|||||||
@freezed
|
@freezed
|
||||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||||
const factory ChatAIMessageEvent.initial() = Initial;
|
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.receiveError(String error) = _ReceiveError;
|
||||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||||
|
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatAIMessageState with _$ChatAIMessageState {
|
class ChatAIMessageState with _$ChatAIMessageState {
|
||||||
const factory ChatAIMessageState({
|
const factory ChatAIMessageState({
|
||||||
AnswerStream? stream,
|
AnswerStream? stream,
|
||||||
String? error,
|
|
||||||
required String text,
|
required String text,
|
||||||
required LoadingState retryState,
|
required MessageState messageState,
|
||||||
}) = _ChatAIMessageState;
|
}) = _ChatAIMessageState;
|
||||||
|
|
||||||
factory ChatAIMessageState.initial(dynamic text) {
|
factory ChatAIMessageState.initial(dynamic text) {
|
||||||
return ChatAIMessageState(
|
return ChatAIMessageState(
|
||||||
text: text is String ? text : "",
|
text: text is String ? text : "",
|
||||||
stream: text is AnswerStream ? text : null,
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef AnswerStreamElement = String;
|
|
||||||
|
|
||||||
class AnswerStream {
|
class AnswerStream {
|
||||||
AnswerStream() {
|
AnswerStream() {
|
||||||
_port.handler = _controller.add;
|
_port.handler = _controller.add;
|
||||||
@ -534,23 +532,53 @@ class AnswerStream {
|
|||||||
(event) {
|
(event) {
|
||||||
if (event.startsWith("data:")) {
|
if (event.startsWith("data:")) {
|
||||||
_hasStarted = true;
|
_hasStarted = true;
|
||||||
|
final newText = event.substring(5);
|
||||||
|
_text += newText;
|
||||||
|
if (_onData != null) {
|
||||||
|
_onData!(_text);
|
||||||
|
}
|
||||||
} else if (event.startsWith("error:")) {
|
} else if (event.startsWith("error:")) {
|
||||||
_error = event.substring(5);
|
_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 RawReceivePort _port = RawReceivePort();
|
||||||
final StreamController<AnswerStreamElement> _controller =
|
final StreamController<String> _controller = StreamController.broadcast();
|
||||||
StreamController.broadcast();
|
late StreamSubscription<String> _subscription;
|
||||||
late StreamSubscription<AnswerStreamElement> _subscription;
|
|
||||||
bool _hasStarted = false;
|
bool _hasStarted = false;
|
||||||
String? _error;
|
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;
|
int get nativePort => _port.sendPort.nativePort;
|
||||||
bool get hasStarted => _hasStarted;
|
bool get hasStarted => _hasStarted;
|
||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
|
String get text => _text;
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _controller.close();
|
await _controller.close();
|
||||||
@ -558,9 +586,23 @@ class AnswerStream {
|
|||||||
_port.close();
|
_port.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<AnswerStreamElement> listen(
|
StreamSubscription<String> listen({
|
||||||
void Function(AnswerStreamElement event)? onData,
|
void Function(String text)? onData,
|
||||||
) {
|
void Function()? onStart,
|
||||||
return _controller.stream.listen(onData);
|
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/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_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/chat_loading.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -38,7 +37,8 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
)..add(const ChatAIMessageEvent.initial()),
|
)..add(const ChatAIMessageEvent.initial()),
|
||||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.error != null) {
|
return state.messageState.when(
|
||||||
|
onError: (err) {
|
||||||
return StreamingError(
|
return StreamingError(
|
||||||
onRetryPressed: () {
|
onRetryPressed: () {
|
||||||
context.read<ChatAIMessageBloc>().add(
|
context.read<ChatAIMessageBloc>().add(
|
||||||
@ -46,18 +46,26 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
onAIResponseLimit: () {
|
||||||
if (state.retryState == const LoadingState.loading()) {
|
return FlowyText(
|
||||||
return const ChatAILoading();
|
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
||||||
}
|
maxLines: 10,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ready: () {
|
||||||
if (state.text.isEmpty) {
|
if (state.text.isEmpty) {
|
||||||
return const ChatAILoading();
|
return const ChatAILoading();
|
||||||
} else {
|
} else {
|
||||||
return AIMarkdownText(markdown: state.text);
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.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/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_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/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_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/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/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/workspace/presentation/home/toast.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/error.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.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/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.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';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
abstract class IEditableSummaryCellSkin {
|
abstract class IEditableSummaryCellSkin {
|
||||||
@ -149,7 +151,22 @@ class SummaryCellAccessory extends StatelessWidget {
|
|||||||
rowId: rowId,
|
rowId: rowId,
|
||||||
fieldId: fieldId,
|
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) {
|
builder: (context, state) {
|
||||||
return const Row(
|
return const Row(
|
||||||
children: [SummaryButton(), HSpace(6), CopyButton()],
|
children: [SummaryButton(), HSpace(6), CopyButton()],
|
||||||
@ -169,13 +186,13 @@ class SummaryButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.loadingState.map(
|
return state.loadingState.when(
|
||||||
loading: (_) {
|
loading: () {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
finish: (_) {
|
finish: () {
|
||||||
return FlowyTooltip(
|
return FlowyTooltip(
|
||||||
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
||||||
child: Container(
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.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/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_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/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_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/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/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/workspace/presentation/home/toast.dart';
|
||||||
|
import 'package:appflowy_backend/dispatch/error.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.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/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.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';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
abstract class IEditableTranslateCellSkin {
|
abstract class IEditableTranslateCellSkin {
|
||||||
@ -150,7 +152,22 @@ class TranslateCellAccessory extends StatelessWidget {
|
|||||||
rowId: rowId,
|
rowId: rowId,
|
||||||
fieldId: fieldId,
|
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) {
|
builder: (context, state) {
|
||||||
return const Row(
|
return const Row(
|
||||||
children: [TranslateButton(), HSpace(6), CopyButton()],
|
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/startup/startup.dart';
|
||||||
import 'package:appflowy/util/file_extension.dart';
|
import 'package:appflowy/util/file_extension.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/application_data_storage.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/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@ -65,7 +65,7 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
|||||||
return (s.url, null);
|
return (s.url, null);
|
||||||
},
|
},
|
||||||
(err) {
|
(err) {
|
||||||
if (err.code == ErrorCode.FileStorageLimitExceeded) {
|
if (err.isStorageLimitExceeded) {
|
||||||
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
|
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
|
||||||
} else {
|
} else {
|
||||||
return (null, err.msg);
|
return (null, err.msg);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.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/ai_client.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.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:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class OpenAIImageWidget extends StatefulWidget {
|
class OpenAIImageWidget extends StatefulWidget {
|
||||||
const OpenAIImageWidget({
|
const OpenAIImageWidget({
|
||||||
|
@ -1,35 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:easy_localization/easy_localization.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) {
|
void showAILimitDialog(BuildContext context, String message) {
|
||||||
showDialog(
|
showConfirmDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
title: LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||||
useRootNavigator: false,
|
description: message,
|
||||||
builder: (dialogContext) => _AILimitDialog(
|
|
||||||
message: message,
|
|
||||||
onOkPressed: () {},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.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/build_context_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.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_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.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:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'ai_limit_dialog.dart';
|
import 'ai_limit_dialog.dart';
|
||||||
@ -46,7 +43,7 @@ Node autoCompletionNode({
|
|||||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||||
getName: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr,
|
getName: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr,
|
||||||
iconData: Icons.generating_tokens,
|
iconData: Icons.generating_tokens,
|
||||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
keywords: ['ai', 'openai', 'writer', 'ai writer', 'autogenerator'],
|
||||||
nodeBuilder: (editorState, _) {
|
nodeBuilder: (editorState, _) {
|
||||||
final node = autoCompletionNode(start: editorState.selection!);
|
final node = autoCompletionNode(start: editorState.selection!);
|
||||||
return node;
|
return node;
|
||||||
@ -130,7 +127,6 @@ class _AutoCompletionBlockComponentState
|
|||||||
_unsubscribeSelectionGesture();
|
_unsubscribeSelectionGesture();
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
textFieldFocusNode.dispose();
|
textFieldFocusNode.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,9 +177,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||||
await editorState.apply(
|
await editorState.apply(
|
||||||
transaction,
|
transaction,
|
||||||
options: const ApplyOptions(
|
options: const ApplyOptions(recordUndo: false),
|
||||||
recordUndo: false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +224,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (error.isLimitExceeded) {
|
if (error.isLimitExceeded) {
|
||||||
showAILimitDialog(context, error.message);
|
showAILimitDialog(context, error.message);
|
||||||
|
await _onDiscard();
|
||||||
} else {
|
} else {
|
||||||
showSnackBarMessage(
|
showSnackBarMessage(
|
||||||
context,
|
context,
|
||||||
@ -417,12 +412,10 @@ class _AutoCompletionBlockComponentState
|
|||||||
// show dialog
|
// show dialog
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (_) => DiscardDialog(
|
||||||
return DiscardDialog(
|
onConfirm: _onDiscard,
|
||||||
onConfirm: () => _onDiscard(),
|
|
||||||
onCancel: () {},
|
onCancel: () {},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else if (controller.text.isEmpty) {
|
} else if (controller.text.isEmpty) {
|
||||||
_onExit();
|
_onExit();
|
||||||
@ -445,9 +438,7 @@ class _AutoCompletionBlockComponentState
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AutoCompletionHeader extends StatelessWidget {
|
class AutoCompletionHeader extends StatelessWidget {
|
||||||
const AutoCompletionHeader({
|
const AutoCompletionHeader({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -471,23 +462,27 @@ class AutoCompletionInputFooter extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
PrimaryTextButton(
|
FlowyTextButton.primary(
|
||||||
LocaleKeys.button_generate.tr(),
|
text: LocaleKeys.button_generate.tr(),
|
||||||
|
context: context,
|
||||||
onPressed: onGenerate,
|
onPressed: onGenerate,
|
||||||
),
|
),
|
||||||
const Space(10, 0),
|
const Space(10, 0),
|
||||||
SecondaryTextButton(
|
FlowyTextButton.secondary(
|
||||||
LocaleKeys.button_cancel.tr(),
|
text: LocaleKeys.button_cancel.tr(),
|
||||||
|
context: context,
|
||||||
onPressed: onExit,
|
onPressed: onExit,
|
||||||
),
|
),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: FlowyText.regular(
|
child: FlowyText.regular(
|
||||||
LocaleKeys.document_plugins_warning.tr(),
|
LocaleKeys.document_plugins_warning.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -512,18 +507,21 @@ class AutoCompletionFooter extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
PrimaryTextButton(
|
FlowyTextButton.primary(
|
||||||
LocaleKeys.button_keep.tr(),
|
context: context,
|
||||||
|
text: LocaleKeys.button_keep.tr(),
|
||||||
onPressed: onKeep,
|
onPressed: onKeep,
|
||||||
),
|
),
|
||||||
const Space(10, 0),
|
const Space(10, 0),
|
||||||
SecondaryTextButton(
|
FlowyTextButton.secondary(
|
||||||
LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
context: context,
|
||||||
|
text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||||
onPressed: onRewrite,
|
onPressed: onRewrite,
|
||||||
),
|
),
|
||||||
const Space(10, 0),
|
const Space(10, 0),
|
||||||
SecondaryTextButton(
|
FlowyTextButton.secondary(
|
||||||
LocaleKeys.button_discard.tr(),
|
context: context,
|
||||||
|
text: LocaleKeys.button_discard.tr(),
|
||||||
onPressed: onDiscard,
|
onPressed: onDiscard,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.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/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/snap_bar.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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@ -239,19 +240,15 @@ class PageStyleCoverImage extends StatelessWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showSnapBar(
|
return showSnapBar(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.document_plugins_image_imageUploadFailed,
|
LocaleKeys.document_plugins_image_imageUploadFailed.tr(),
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.read<DocumentPageStyleBloc>().add(
|
context.read<DocumentPageStyleBloc>().add(
|
||||||
DocumentPageStyleEvent.updateCoverImage(
|
DocumentPageStyleEvent.updateCoverImage(
|
||||||
PageStyleCover(
|
PageStyleCover(type: type, value: result),
|
||||||
type: type,
|
|
||||||
value: result,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -282,10 +279,7 @@ class PageStyleCoverImage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80),
|
||||||
maxHeight: maxHeight,
|
|
||||||
minHeight: 80,
|
|
||||||
),
|
|
||||||
child: BlocProvider.value(
|
child: BlocProvider.value(
|
||||||
value: pageStyleBloc,
|
value: pageStyleBloc,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||||
import 'package:appflowy/plugins/shared/share/share_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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class ShareMenuButton extends StatelessWidget {
|
class ShareMenuButton extends StatelessWidget {
|
||||||
|
@ -91,7 +91,7 @@ enum FeatureFlag {
|
|||||||
|
|
||||||
bool get isOn {
|
bool get isOn {
|
||||||
if ([
|
if ([
|
||||||
FeatureFlag.planBilling,
|
// FeatureFlag.planBilling,
|
||||||
// release this feature in version 0.6.1
|
// release this feature in version 0.6.1
|
||||||
FeatureFlag.spaceDesign,
|
FeatureFlag.spaceDesign,
|
||||||
// release this feature in version 0.5.9
|
// release this feature in version 0.5.9
|
||||||
|
@ -90,7 +90,7 @@ class CompletionStream {
|
|||||||
if (event == "AI_RESPONSE_LIMIT") {
|
if (event == "AI_RESPONSE_LIMIT") {
|
||||||
onError(
|
onError(
|
||||||
AIError(
|
AIError(
|
||||||
message: LocaleKeys.sideBar_aiResponseLitmit.tr(),
|
message: LocaleKeys.sideBar_aiResponseLimit.tr(),
|
||||||
code: AIErrorCode.aiResponseLimitExceeded,
|
code: AIErrorCode.aiResponseLimitExceeded,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
@ -14,6 +14,7 @@ abstract class IUserBackendService {
|
|||||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
SubscriptionPlanPB plan,
|
SubscriptionPlanPB plan,
|
||||||
|
String? reason,
|
||||||
);
|
);
|
||||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
@ -21,6 +22,9 @@ abstract class IUserBackendService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _baseBetaUrl = 'https://beta.appflowy.com';
|
||||||
|
const _baseProdUrl = 'https://appflowy.com';
|
||||||
|
|
||||||
class UserBackendService implements IUserBackendService {
|
class UserBackendService implements IUserBackendService {
|
||||||
UserBackendService({required this.userId});
|
UserBackendService({required this.userId});
|
||||||
|
|
||||||
@ -255,19 +259,24 @@ class UserBackendService implements IUserBackendService {
|
|||||||
..recurringInterval = RecurringIntervalPB.Year
|
..recurringInterval = RecurringIntervalPB.Year
|
||||||
..workspaceSubscriptionPlan = plan
|
..workspaceSubscriptionPlan = plan
|
||||||
..successUrl =
|
..successUrl =
|
||||||
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success?plan=${plan.toRecognizable()}';
|
'${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}';
|
||||||
return UserEventSubscribeWorkspace(request).send();
|
return UserEventSubscribeWorkspace(request).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
SubscriptionPlanPB plan,
|
SubscriptionPlanPB plan, [
|
||||||
) {
|
String? reason,
|
||||||
|
]) {
|
||||||
final request = CancelWorkspaceSubscriptionPB()
|
final request = CancelWorkspaceSubscriptionPB()
|
||||||
..workspaceId = workspaceId
|
..workspaceId = workspaceId
|
||||||
..plan = plan;
|
..plan = plan;
|
||||||
|
|
||||||
|
if (reason != null) {
|
||||||
|
request.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
return UserEventCancelWorkspaceSubscription(request).send();
|
return UserEventCancelWorkspaceSubscription(request).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class SettingsBillingBloc
|
|||||||
(f) => Log.error(f.msg, f),
|
(f) => Log.error(f.msg, f),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cancelSubscription: (plan) async {
|
cancelSubscription: (plan, reason) async {
|
||||||
final s = state.mapOrNull(ready: (s) => s);
|
final s = state.mapOrNull(ready: (s) => s);
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return;
|
return;
|
||||||
@ -124,7 +124,7 @@ class SettingsBillingBloc
|
|||||||
emit(s.copyWith(isLoading: true));
|
emit(s.copyWith(isLoading: true));
|
||||||
|
|
||||||
final result =
|
final result =
|
||||||
await _userService.cancelSubscription(workspaceId, plan);
|
await _userService.cancelSubscription(workspaceId, plan, reason);
|
||||||
final successOrNull = result.fold(
|
final successOrNull = result.fold(
|
||||||
(_) => true,
|
(_) => true,
|
||||||
(f) {
|
(f) {
|
||||||
@ -276,8 +276,9 @@ class SettingsBillingEvent with _$SettingsBillingEvent {
|
|||||||
_AddSubscription;
|
_AddSubscription;
|
||||||
|
|
||||||
const factory SettingsBillingEvent.cancelSubscription(
|
const factory SettingsBillingEvent.cancelSubscription(
|
||||||
SubscriptionPlanPB plan,
|
SubscriptionPlanPB plan, {
|
||||||
) = _CancelSubscription;
|
@Default(null) String? reason,
|
||||||
|
}) = _CancelSubscription;
|
||||||
|
|
||||||
const factory SettingsBillingEvent.paymentSuccessful({
|
const factory SettingsBillingEvent.paymentSuccessful({
|
||||||
SubscriptionPlanPB? plan,
|
SubscriptionPlanPB? plan,
|
||||||
|
@ -95,7 +95,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cancelSubscription: () async {
|
cancelSubscription: (reason) async {
|
||||||
final newState = state
|
final newState = state
|
||||||
.mapOrNull(ready: (state) => state)
|
.mapOrNull(ready: (state) => state)
|
||||||
?.copyWith(downgradeProcessing: true);
|
?.copyWith(downgradeProcessing: true);
|
||||||
@ -106,6 +106,7 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
|||||||
final result = await _userService.cancelSubscription(
|
final result = await _userService.cancelSubscription(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
SubscriptionPlanPB.Pro,
|
SubscriptionPlanPB.Pro,
|
||||||
|
reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
final successOrNull = result.fold(
|
final successOrNull = result.fold(
|
||||||
@ -206,7 +207,9 @@ class SettingsPlanEvent with _$SettingsPlanEvent {
|
|||||||
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
|
||||||
_AddSubscription;
|
_AddSubscription;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
|
const factory SettingsPlanEvent.cancelSubscription({
|
||||||
|
@Default(null) String? reason,
|
||||||
|
}) = _CancelSubscription;
|
||||||
|
|
||||||
const factory SettingsPlanEvent.paymentSuccessful({
|
const factory SettingsPlanEvent.paymentSuccessful({
|
||||||
@Default(null) SubscriptionPlanPB? plan,
|
@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:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
extension SubscriptionInfoHelpers on WorkspaceSubscriptionInfoPB {
|
||||||
String get label => switch (plan) {
|
String get label => switch (plan) {
|
||||||
WorkspacePlanPB.FreePlan =>
|
WorkspacePlanPB.FreePlan =>
|
||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
|
||||||
@ -24,6 +24,14 @@ extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
|
|||||||
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
|
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
|
||||||
_ => 'N/A',
|
_ => 'N/A',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool get isBillingPortalEnabled {
|
||||||
|
if (plan != WorkspacePlanPB.FreePlan || addOns.isNotEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AllSubscriptionLabels on SubscriptionPlanPB {
|
extension AllSubscriptionLabels on SubscriptionPlanPB {
|
||||||
|
@ -16,7 +16,7 @@ part 'sidebar_plan_bloc.freezed.dart';
|
|||||||
|
|
||||||
class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||||
SidebarPlanBloc() : super(const 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>();
|
final subscriptionListener = getIt<SubscriptionSuccessListenable>();
|
||||||
subscriptionListener.addListener(() {
|
subscriptionListener.addListener(() {
|
||||||
final plan = subscriptionListener.subscribedPlan;
|
final plan = subscriptionListener.subscribedPlan;
|
||||||
@ -49,6 +49,7 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. Listen to the storage notification
|
||||||
_storageListener = StoreageNotificationListener(
|
_storageListener = StoreageNotificationListener(
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
@ -57,6 +58,7 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. Listen to specific error codes
|
||||||
_globalErrorListener = GlobalErrorCodeNotifier.add(
|
_globalErrorListener = GlobalErrorCodeNotifier.add(
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
@ -92,11 +94,21 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
|||||||
) async {
|
) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
receiveError: (FlowyError error) async {
|
receiveError: (FlowyError error) async {
|
||||||
|
if (error.code == ErrorCode.AIResponseLimitExceeded) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
tierIndicator: const SidebarToastTierIndicator.aiMaxiLimitHit(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (error.code == ErrorCode.FileStorageLimitExceeded) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
tierIndicator: const SidebarToastTierIndicator.storageLimitHit(),
|
tierIndicator: const SidebarToastTierIndicator.storageLimitHit(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
Log.error("Unhandle Unexpected error: $error");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
init: (String workspaceId, UserProfilePB userProfile) {
|
init: (String workspaceId, UserProfilePB userProfile) {
|
||||||
emit(
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.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:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SidebarFooter extends StatelessWidget {
|
class SidebarFooter extends StatelessWidget {
|
||||||
const SidebarFooter({super.key});
|
const SidebarFooter({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Row(
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (FeatureFlag.planBilling.isOn) const SidebarToast(),
|
||||||
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: SidebarTrashButton()),
|
Expanded(child: SidebarTrashButton()),
|
||||||
// Enable it when the widget button is ready
|
// Enable it when the widget button is ready
|
||||||
@ -24,14 +30,14 @@ class SidebarFooter extends StatelessWidget {
|
|||||||
// ),
|
// ),
|
||||||
// Expanded(child: SidebarWidgetButton()),
|
// Expanded(child: SidebarWidgetButton()),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SidebarTrashButton extends StatelessWidget {
|
class SidebarTrashButton extends StatelessWidget {
|
||||||
const SidebarTrashButton({
|
const SidebarTrashButton({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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/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/settings/settings_dialog_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_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/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SidebarToast extends StatefulWidget {
|
class SidebarToast extends StatelessWidget {
|
||||||
const SidebarToast({super.key});
|
const SidebarToast({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<SidebarToast> createState() => _SidebarToastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SidebarToastState extends State<SidebarToast> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocConsumer<SidebarPlanBloc, SidebarPlanState>(
|
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.
|
// 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.
|
// Even though the dislog is dissmissed, if the user triggers the storage limit again, the dialog will show again.
|
||||||
state.tierIndicator.maybeWhen(
|
state.tierIndicator.maybeWhen(
|
||||||
storageLimitHit: () {
|
storageLimitHit: () => WidgetsBinding.instance.addPostFrameCallback(
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) => _showStorageLimitDialog(context),
|
(_) => _showStorageLimitDialog(context),
|
||||||
debugLabel: 'Sidebar.showStorageLimit',
|
),
|
||||||
|
orElse: () {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
orElse: () {
|
builder: (_, state) {
|
||||||
// Do nothing
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
|
||||||
return BlocBuilder<SidebarPlanBloc, SidebarPlanState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return state.tierIndicator.when(
|
return state.tierIndicator.when(
|
||||||
storageLimitHit: () => Column(
|
loading: () => const SizedBox.shrink(),
|
||||||
children: [
|
storageLimitHit: () => PlanIndicator(
|
||||||
const Divider(height: 0.6),
|
planName: SubscriptionPlanPB.Free.label,
|
||||||
PlanIndicator(
|
|
||||||
planName: "Pro",
|
|
||||||
text: LocaleKeys.sideBar_upgradeToPro.tr(),
|
text: LocaleKeys.sideBar_upgradeToPro.tr(),
|
||||||
onTap: () {
|
onTap: () => _hanldeOnTap(context, SubscriptionPlanPB.Pro),
|
||||||
_hanldeOnTap(context, SubscriptionPlanPB.Pro);
|
|
||||||
},
|
|
||||||
reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
||||||
),
|
),
|
||||||
],
|
aiMaxiLimitHit: () => PlanIndicator(
|
||||||
),
|
planName: SubscriptionPlanPB.AiMax.label,
|
||||||
aiMaxiLimitHit: () => Column(
|
|
||||||
children: [
|
|
||||||
const Divider(height: 0.6),
|
|
||||||
PlanIndicator(
|
|
||||||
planName: "AI Max",
|
|
||||||
text: LocaleKeys.sideBar_upgradeToAIMax.tr(),
|
text: LocaleKeys.sideBar_upgradeToAIMax.tr(),
|
||||||
onTap: () {
|
onTap: () => _hanldeOnTap(context, SubscriptionPlanPB.AiMax),
|
||||||
_hanldeOnTap(context, SubscriptionPlanPB.AiMax);
|
reason: LocaleKeys.sideBar_aiResponseLimitTitle.tr(),
|
||||||
},
|
|
||||||
reason: LocaleKeys.sideBar_aiResponseLitmitDialogTitle.tr(),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
loading: () {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showStorageLimitDialog(BuildContext context) {
|
void _showStorageLimitDialog(BuildContext context) => showConfirmDialog(
|
||||||
showDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
title: LocaleKeys.sideBar_purchaseStorageSpace.tr(),
|
||||||
useRootNavigator: false,
|
description: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
||||||
builder: (dialogContext) => _StorageLimitDialog(
|
confirmLabel:
|
||||||
onOkPressed: () {
|
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;
|
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>();
|
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(
|
showSettingsDialog(
|
||||||
context,
|
context,
|
||||||
userProfile,
|
userProfile,
|
||||||
@ -95,36 +91,30 @@ class _SidebarToastState extends State<SidebarToast> {
|
|||||||
SettingsPage.plan,
|
SettingsPage.plan,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.error(
|
final message = plan == SubscriptionPlanPB.AiMax
|
||||||
"UserProfile is null. It should not happen. If you see this error, it's a bug.",
|
? LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr()
|
||||||
);
|
: LocaleKeys.sideBar_askOwnerToUpgradeToPro.tr();
|
||||||
}
|
|
||||||
},
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
useRootNavigator: false,
|
||||||
|
builder: (dialogContext) => _AskOwnerToChangePlan(
|
||||||
|
message: message,
|
||||||
|
onOkPressed: () {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlanIndicator extends StatelessWidget {
|
class PlanIndicator extends StatefulWidget {
|
||||||
const PlanIndicator({
|
const PlanIndicator({
|
||||||
|
super.key,
|
||||||
required this.planName,
|
required this.planName,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.reason,
|
required this.reason,
|
||||||
super.key,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final String planName;
|
final String planName;
|
||||||
@ -132,62 +122,150 @@ class PlanIndicator extends StatelessWidget {
|
|||||||
final String text;
|
final String text;
|
||||||
final Function() onTap;
|
final Function() onTap;
|
||||||
|
|
||||||
final textColor = const Color(0xFFE8E2EE);
|
@override
|
||||||
final secondaryColor = const Color(0xFF653E8C);
|
State<PlanIndicator> createState() => _PlanIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlanIndicatorState extends State<PlanIndicator> {
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
popoverController.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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: [
|
children: [
|
||||||
FlowyButton(
|
FlowyText(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
widget.text,
|
||||||
text: FlowyText(
|
color: AFThemeExtension.of(context).strongText,
|
||||||
text,
|
|
||||||
color: textColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
),
|
||||||
radius: BorderRadius.zero,
|
const VSpace(12),
|
||||||
leftIconSize: const Size(40, 20),
|
Opacity(
|
||||||
leftIcon: Badge(
|
opacity: 0.7,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
child: FlowyText.regular(
|
||||||
backgroundColor: secondaryColor,
|
widget.reason,
|
||||||
label: FlowyText.semibold(
|
maxLines: null,
|
||||||
planName,
|
lineHeight: 1.3,
|
||||||
fontSize: 12,
|
textAlign: TextAlign.center,
|
||||||
color: textColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 6),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
child: Opacity(
|
borderRadius: BorderRadius.circular(9),
|
||||||
opacity: 0.4,
|
),
|
||||||
|
child: Center(
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
reason,
|
LocaleKeys
|
||||||
textAlign: TextAlign.start,
|
.settings_comparePlanDialog_actions_upgrade
|
||||||
color: textColor,
|
.tr(),
|
||||||
fontSize: 8,
|
color: Colors.white,
|
||||||
maxLines: 10,
|
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 {
|
class _AskOwnerToChangePlan extends StatelessWidget {
|
||||||
const _StorageLimitDialog({
|
const _AskOwnerToChangePlan({
|
||||||
|
required this.message,
|
||||||
required this.onOkPressed,
|
required this.onOkPressed,
|
||||||
});
|
});
|
||||||
|
final String message;
|
||||||
final VoidCallback onOkPressed;
|
final VoidCallback onOkPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return NavigatorOkCancelDialog(
|
return NavigatorOkCancelDialog(
|
||||||
message: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
|
message: message,
|
||||||
okTitle: LocaleKeys.sideBar_purchaseStorageSpace.tr(),
|
okTitle: LocaleKeys.button_ok.tr(),
|
||||||
onOkPressed: onOkPressed,
|
onOkPressed: onOkPressed,
|
||||||
titleUpperCase: false,
|
titleUpperCase: false,
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/startup/startup.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/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/blank/blank.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/button.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
Loading? _duplicateSpaceLoading;
|
Loading? _duplicateSpaceLoading;
|
||||||
@ -358,9 +359,6 @@ class _SidebarState extends State<_Sidebar> {
|
|||||||
child: const SidebarFooter(),
|
child: const SidebarFooter(),
|
||||||
),
|
),
|
||||||
const VSpace(14),
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SpaceMigration extends StatefulWidget {
|
class SpaceMigration extends StatefulWidget {
|
||||||
@ -70,14 +71,8 @@ class _SpaceMigrationState extends State<SpaceMigration> {
|
|||||||
const linearGradient = LinearGradient(
|
const linearGradient = LinearGradient(
|
||||||
begin: Alignment.bottomLeft,
|
begin: Alignment.bottomLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],
|
||||||
Color(0xFF8032FF),
|
stops: [0.1545, 0.8225],
|
||||||
Color(0xFFEF35FF),
|
|
||||||
],
|
|
||||||
stops: [
|
|
||||||
0.1545,
|
|
||||||
0.8225,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
|
@ -91,7 +91,7 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
|
|||||||
},
|
},
|
||||||
ready: (state) {
|
ready: (state) {
|
||||||
final billingPortalEnabled =
|
final billingPortalEnabled =
|
||||||
state.subscriptionInfo.plan != WorkspacePlanPB.FreePlan;
|
state.subscriptionInfo.isBillingPortalEnabled;
|
||||||
|
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
title: LocaleKeys.settings_billingPage_title.tr(),
|
title: LocaleKeys.settings_billingPage_title.tr(),
|
||||||
@ -327,14 +327,9 @@ class _AITileState extends State<_AITile> {
|
|||||||
: LocaleKeys.settings_billingPage_addons_addLabel.tr(),
|
: LocaleKeys.settings_billingPage_addons_addLabel.tr(),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
minWidth: _buttonsMinWidth,
|
minWidth: _buttonsMinWidth,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (widget.subscriptionInfo != null && isCanceled) {
|
if (widget.subscriptionInfo != null) {
|
||||||
// Show customer portal to renew
|
await showConfirmDialog(
|
||||||
context
|
|
||||||
.read<SettingsBillingBloc>()
|
|
||||||
.add(const SettingsBillingEvent.openCustomerPortal());
|
|
||||||
} else if (widget.subscriptionInfo != null) {
|
|
||||||
showConfirmDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
style: ConfirmPopupStyle.cancelAndOk,
|
style: ConfirmPopupStyle.cancelAndOk,
|
||||||
title: LocaleKeys.settings_billingPage_addons_removeDialog_title
|
title: LocaleKeys.settings_billingPage_addons_removeDialog_title
|
||||||
@ -343,11 +338,9 @@ class _AITileState extends State<_AITile> {
|
|||||||
.settings_billingPage_addons_removeDialog_description
|
.settings_billingPage_addons_removeDialog_description
|
||||||
.tr(namedArgs: {"plan": widget.plan.label.tr()}),
|
.tr(namedArgs: {"plan": widget.plan.label.tr()}),
|
||||||
confirmLabel: LocaleKeys.button_confirm.tr(),
|
confirmLabel: LocaleKeys.button_confirm.tr(),
|
||||||
onConfirm: () {
|
onConfirm: () => context
|
||||||
context.read<SettingsBillingBloc>().add(
|
.read<SettingsBillingBloc>()
|
||||||
SettingsBillingEvent.cancelSubscription(widget.plan),
|
.add(SettingsBillingEvent.cancelSubscription(widget.plan)),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Add the addon
|
// 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/settings_plan_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.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/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/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -141,7 +142,7 @@ class _SettingsPlanComparisonDialogState
|
|||||||
children: [
|
children: [
|
||||||
const VSpace(30),
|
const VSpace(30),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 100,
|
height: 116,
|
||||||
child: FlowyText.semibold(
|
child: FlowyText.semibold(
|
||||||
LocaleKeys
|
LocaleKeys
|
||||||
.settings_comparePlanDialog_planFeatures
|
.settings_comparePlanDialog_planFeatures
|
||||||
@ -153,7 +154,7 @@ class _SettingsPlanComparisonDialogState
|
|||||||
: const Color(0xFFE8E0FF),
|
: const Color(0xFFE8E0FF),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 96),
|
const SizedBox(height: 116),
|
||||||
const SizedBox(height: 56),
|
const SizedBox(height: 56),
|
||||||
..._planLabels.map(
|
..._planLabels.map(
|
||||||
(e) => _ComparisonCell(
|
(e) => _ComparisonCell(
|
||||||
@ -184,17 +185,9 @@ class _SettingsPlanComparisonDialogState
|
|||||||
cells: _freeLabels,
|
cells: _freeLabels,
|
||||||
isCurrent:
|
isCurrent:
|
||||||
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
||||||
canDowngrade:
|
buttonType: WorkspacePlanPB.FreePlan.buttonTypeFor(
|
||||||
currentInfo.plan != WorkspacePlanPB.FreePlan,
|
currentInfo.plan,
|
||||||
currentCanceled: currentInfo.isCanceled ||
|
),
|
||||||
(context
|
|
||||||
.watch<SettingsPlanBloc>()
|
|
||||||
.state
|
|
||||||
.mapOrNull(
|
|
||||||
loading: (_) => true,
|
|
||||||
ready: (s) => s.downgradeProcessing,
|
|
||||||
) ??
|
|
||||||
false),
|
|
||||||
onSelected: () async {
|
onSelected: () async {
|
||||||
if (currentInfo.plan ==
|
if (currentInfo.plan ==
|
||||||
WorkspacePlanPB.FreePlan ||
|
WorkspacePlanPB.FreePlan ||
|
||||||
@ -202,6 +195,12 @@ class _SettingsPlanComparisonDialogState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final reason =
|
||||||
|
await showCancelSurveyDialog(context);
|
||||||
|
if (reason == null || !context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await showConfirmDialog(
|
await showConfirmDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: LocaleKeys
|
title: LocaleKeys
|
||||||
@ -216,8 +215,9 @@ class _SettingsPlanComparisonDialogState
|
|||||||
style: ConfirmPopupStyle.cancelAndOk,
|
style: ConfirmPopupStyle.cancelAndOk,
|
||||||
onConfirm: () =>
|
onConfirm: () =>
|
||||||
context.read<SettingsPlanBloc>().add(
|
context.read<SettingsPlanBloc>().add(
|
||||||
const SettingsPlanEvent
|
SettingsPlanEvent.cancelSubscription(
|
||||||
.cancelSubscription(),
|
reason: reason,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -242,9 +242,9 @@ class _SettingsPlanComparisonDialogState
|
|||||||
cells: _proLabels,
|
cells: _proLabels,
|
||||||
isCurrent:
|
isCurrent:
|
||||||
currentInfo.plan == WorkspacePlanPB.ProPlan,
|
currentInfo.plan == WorkspacePlanPB.ProPlan,
|
||||||
canUpgrade:
|
buttonType: WorkspacePlanPB.ProPlan.buttonTypeFor(
|
||||||
currentInfo.plan == WorkspacePlanPB.FreePlan,
|
currentInfo.plan,
|
||||||
currentCanceled: currentInfo.isCanceled,
|
),
|
||||||
onSelected: () =>
|
onSelected: () =>
|
||||||
context.read<SettingsPlanBloc>().add(
|
context.read<SettingsPlanBloc>().add(
|
||||||
const SettingsPlanEvent.addSubscription(
|
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 {
|
class _PlanTable extends StatelessWidget {
|
||||||
const _PlanTable({
|
const _PlanTable({
|
||||||
required this.title,
|
required this.title,
|
||||||
@ -275,9 +304,7 @@ class _PlanTable extends StatelessWidget {
|
|||||||
required this.cells,
|
required this.cells,
|
||||||
required this.isCurrent,
|
required this.isCurrent,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
this.canUpgrade = false,
|
this.buttonType = _PlanButtonType.none,
|
||||||
this.canDowngrade = false,
|
|
||||||
this.currentCanceled = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@ -288,13 +315,11 @@ class _PlanTable extends StatelessWidget {
|
|||||||
final List<_CellItem> cells;
|
final List<_CellItem> cells;
|
||||||
final bool isCurrent;
|
final bool isCurrent;
|
||||||
final VoidCallback onSelected;
|
final VoidCallback onSelected;
|
||||||
final bool canUpgrade;
|
final _PlanButtonType buttonType;
|
||||||
final bool canDowngrade;
|
|
||||||
final bool currentCanceled;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final highlightPlan = !isCurrent && !canDowngrade && canUpgrade;
|
final highlightPlan = !isCurrent && buttonType == _PlanButtonType.upgrade;
|
||||||
final isLM = Theme.of(context).isLightMode;
|
final isLM = Theme.of(context).isLightMode;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@ -336,37 +361,29 @@ class _PlanTable extends StatelessWidget {
|
|||||||
title: price,
|
title: price,
|
||||||
description: priceInfo,
|
description: priceInfo,
|
||||||
isPrimary: !highlightPlan,
|
isPrimary: !highlightPlan,
|
||||||
height: 96,
|
|
||||||
),
|
),
|
||||||
if (canUpgrade || canDowngrade) ...[
|
if (buttonType == _PlanButtonType.none) ...[
|
||||||
|
const SizedBox(height: 56),
|
||||||
|
] else ...[
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: canDowngrade && currentCanceled ? 0.5 : 1,
|
opacity: 1,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: 12 + (canUpgrade && !canDowngrade ? 12 : 0),
|
left: 12 + (buttonType.isUpgrade ? 12 : 0),
|
||||||
),
|
),
|
||||||
child: _ActionButton(
|
child: _ActionButton(
|
||||||
label: canUpgrade && !canDowngrade
|
label: buttonType.isUpgrade
|
||||||
? LocaleKeys.settings_comparePlanDialog_actions_upgrade
|
? LocaleKeys.settings_comparePlanDialog_actions_upgrade
|
||||||
.tr()
|
.tr()
|
||||||
: LocaleKeys
|
: LocaleKeys
|
||||||
.settings_comparePlanDialog_actions_downgrade
|
.settings_comparePlanDialog_actions_downgrade
|
||||||
.tr(),
|
.tr(),
|
||||||
onPressed: !canUpgrade && canDowngrade && currentCanceled
|
onPressed: onSelected,
|
||||||
? null
|
isUpgrade: buttonType.isUpgrade,
|
||||||
: onSelected,
|
useGradientBorder: buttonType.isUpgrade,
|
||||||
tooltip: !canUpgrade && canDowngrade && currentCanceled
|
|
||||||
? LocaleKeys
|
|
||||||
.settings_comparePlanDialog_actions_downgradeDisabledTooltip
|
|
||||||
.tr()
|
|
||||||
: null,
|
|
||||||
isUpgrade: canUpgrade && !canDowngrade,
|
|
||||||
useGradientBorder: !isCurrent && canUpgrade,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
|
||||||
const SizedBox(height: 56),
|
|
||||||
],
|
],
|
||||||
...cells.map(
|
...cells.map(
|
||||||
(cell) => _ComparisonCell(
|
(cell) => _ComparisonCell(
|
||||||
@ -467,14 +484,12 @@ class _ComparisonCell extends StatelessWidget {
|
|||||||
class _ActionButton extends StatelessWidget {
|
class _ActionButton extends StatelessWidget {
|
||||||
const _ActionButton({
|
const _ActionButton({
|
||||||
required this.label,
|
required this.label,
|
||||||
this.tooltip,
|
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.isUpgrade,
|
required this.isUpgrade,
|
||||||
this.useGradientBorder = false,
|
this.useGradientBorder = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final String? tooltip;
|
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final bool isUpgrade;
|
final bool isUpgrade;
|
||||||
final bool useGradientBorder;
|
final bool useGradientBorder;
|
||||||
@ -487,9 +502,7 @@ class _ActionButton extends StatelessWidget {
|
|||||||
height: 56,
|
height: 56,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
FlowyTooltip(
|
GestureDetector(
|
||||||
message: tooltip,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onPressed,
|
onTap: onPressed,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: onPressed != null
|
cursor: onPressed != null
|
||||||
@ -514,7 +527,6 @@ class _ActionButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -538,10 +550,7 @@ class _ActionButton extends StatelessWidget {
|
|||||||
shaderCallback: (bounds) => const LinearGradient(
|
shaderCallback: (bounds) => const LinearGradient(
|
||||||
transform: GradientRotation(-1.55),
|
transform: GradientRotation(-1.55),
|
||||||
stops: [0.4, 1],
|
stops: [0.4, 1],
|
||||||
colors: [
|
colors: [Color(0xFF251D37), Color(0xFF7547C0)],
|
||||||
Color(0xFF251D37),
|
|
||||||
Color(0xFF7547C0),
|
|
||||||
],
|
|
||||||
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@ -579,19 +588,17 @@ class _Heading extends StatelessWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
this.description,
|
this.description,
|
||||||
this.isPrimary = true,
|
this.isPrimary = true,
|
||||||
this.height = 100,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final bool isPrimary;
|
final bool isPrimary;
|
||||||
final double height;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 185,
|
width: 185,
|
||||||
height: height,
|
height: 116,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
|
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -615,12 +622,14 @@ class _Heading extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (description != null && description!.isNotEmpty) ...[
|
if (description != null && description!.isNotEmpty) ...[
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
FlowyText.regular(
|
Flexible(
|
||||||
|
child: FlowyText.regular(
|
||||||
description!,
|
description!,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
maxLines: 3,
|
maxLines: 5,
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -232,7 +232,7 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
|
|||||||
const VSpace(8),
|
const VSpace(8),
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
widget.subscriptionInfo.info,
|
widget.subscriptionInfo.info,
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
color: AFThemeExtension.of(context).strongText,
|
color: AFThemeExtension.of(context).strongText,
|
||||||
maxLines: 3,
|
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 actionResult = state.actionResult!.result;
|
||||||
final actionType = state.actionResult!.actionType;
|
final actionType = state.actionResult!.actionType;
|
||||||
|
|
||||||
debugPrint("Plan: ${state.subscriptionInfo?.plan}");
|
|
||||||
|
|
||||||
if (actionType == WorkspaceMemberActionType.invite &&
|
if (actionType == WorkspaceMemberActionType.invite &&
|
||||||
actionResult.isFailure) {
|
actionResult.isFailure) {
|
||||||
final error = actionResult.getFailure().code;
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
@ -111,26 +112,26 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
// if (FeatureFlag.planBilling.isOn &&
|
if (FeatureFlag.planBilling.isOn &&
|
||||||
// userProfile.authenticator ==
|
userProfile.authenticator ==
|
||||||
// AuthenticatorPB.AppFlowyCloud &&
|
AuthenticatorPB.AppFlowyCloud &&
|
||||||
// member != null &&
|
member != null &&
|
||||||
// member!.role.isOwner) ...[
|
member!.role.isOwner) ...[
|
||||||
// SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
// page: SettingsPage.plan,
|
page: SettingsPage.plan,
|
||||||
// selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
// label: LocaleKeys.settings_planPage_menuLabel.tr(),
|
label: LocaleKeys.settings_planPage_menuLabel.tr(),
|
||||||
// icon: const FlowySvg(FlowySvgs.settings_plan_m),
|
icon: const FlowySvg(FlowySvgs.settings_plan_m),
|
||||||
// changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
// ),
|
),
|
||||||
// SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
// page: SettingsPage.billing,
|
page: SettingsPage.billing,
|
||||||
// selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
// label: LocaleKeys.settings_billingPage_menuLabel.tr(),
|
label: LocaleKeys.settings_billingPage_menuLabel.tr(),
|
||||||
// icon: const FlowySvg(FlowySvgs.settings_billing_m),
|
icon: const FlowySvg(FlowySvgs.settings_billing_m),
|
||||||
// changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
// ),
|
),
|
||||||
// ],
|
],
|
||||||
if (kDebugMode)
|
if (kDebugMode)
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
// no need to translate this page
|
// no need to translate this page
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.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:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@ -52,6 +53,8 @@ class StackTraceError {
|
|||||||
|
|
||||||
typedef void ErrorListener();
|
typedef void ErrorListener();
|
||||||
|
|
||||||
|
/// Receive error when Rust backend send error message back to the flutter frontend
|
||||||
|
///
|
||||||
class GlobalErrorCodeNotifier extends ChangeNotifier {
|
class GlobalErrorCodeNotifier extends ChangeNotifier {
|
||||||
// Static instance with lazy initialization
|
// Static instance with lazy initialization
|
||||||
static final GlobalErrorCodeNotifier _instance =
|
static final GlobalErrorCodeNotifier _instance =
|
||||||
@ -107,3 +110,10 @@ class GlobalErrorCodeNotifier extends ChangeNotifier {
|
|||||||
_instance.removeListener(listener);
|
_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:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra/size.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/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
|
||||||
@ -168,6 +169,37 @@ class FlowyTextButton extends StatelessWidget {
|
|||||||
this.borderColor,
|
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 String text;
|
||||||
final FontWeight? fontWeight;
|
final FontWeight? fontWeight;
|
||||||
final Color? fontColor;
|
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",
|
"favoriteSpace": "Favorites",
|
||||||
"RecentSpace": "Recent",
|
"RecentSpace": "Recent",
|
||||||
"Spaces": "Spaces",
|
"Spaces": "Spaces",
|
||||||
"upgradeToPro": "Upgrade to Pro Plan",
|
"upgradeToPro": "Upgrade to Pro",
|
||||||
"upgradeToAIMax": "Unlock unlimited AI",
|
"upgradeToAIMax": "Unlock unlimited AI",
|
||||||
"storageLimitDialogTitle": "You are running out of storage space. Upgrade to Pro Plan to get more storage",
|
"storageLimitDialogTitle": "You have run out of free storage. Upgrade to unlock unlimited storage",
|
||||||
"aiResponseLitmitDialogTitle": "You are running out of AI responses. Upgrade to Pro Plan or AI Max to get more AI responses",
|
"aiResponseLimitTitle": "You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses",
|
||||||
"aiResponseLitmit": "You are running out of AI responses. Go to Settings -> Plan -> Click AI Max or Pro Plan to get more AI 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",
|
"purchaseStorageSpace": "Purchase Storage Space",
|
||||||
"purchaseAIResponse": "Purchase ",
|
"purchaseAIResponse": "Purchase ",
|
||||||
"upgradeToAILocal": "AI offline on your device"
|
"upgradeToAILocal": "AI offline on your device"
|
||||||
@ -350,7 +353,10 @@
|
|||||||
"signInDiscord": "Continue with Discord",
|
"signInDiscord": "Continue with Discord",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous",
|
||||||
|
"submit": "Submit"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"welcome": "Welcome!",
|
"welcome": "Welcome!",
|
||||||
@ -638,7 +644,7 @@
|
|||||||
"menuLabel": "AI Settings",
|
"menuLabel": "AI Settings",
|
||||||
"keys": {
|
"keys": {
|
||||||
"enableAISearchTitle": "AI Search",
|
"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",
|
"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",
|
"llmModel": "Language Model",
|
||||||
"llmModelType": "Language Model Type",
|
"llmModelType": "Language Model Type",
|
||||||
@ -704,14 +710,14 @@
|
|||||||
"title": "AI Max",
|
"title": "AI Max",
|
||||||
"description": "Unlock unlimited AI",
|
"description": "Unlock unlimited AI",
|
||||||
"price": "{}",
|
"price": "{}",
|
||||||
"priceInfo": "/user per month",
|
"priceInfo": "per user per month",
|
||||||
"billingInfo": "billed annually or {} billed monthly"
|
"billingInfo": "billed annually or {} billed monthly"
|
||||||
},
|
},
|
||||||
"aiOnDevice": {
|
"aiOnDevice": {
|
||||||
"title": "AI On-device",
|
"title": "AI On-device",
|
||||||
"description": "AI offline on your device",
|
"description": "AI offline on your device",
|
||||||
"price": "{}",
|
"price": "{}",
|
||||||
"priceInfo": "/user per month",
|
"priceInfo": "per user per month",
|
||||||
"billingInfo": "billed annually or {} billed monthly"
|
"billingInfo": "billed annually or {} billed monthly"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -776,7 +782,6 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"upgrade": "Upgrade",
|
"upgrade": "Upgrade",
|
||||||
"downgrade": "Downgrade",
|
"downgrade": "Downgrade",
|
||||||
"downgradeDisabledTooltip": "You will automatically downgrade at the end of the billing cycle",
|
|
||||||
"current": "Current"
|
"current": "Current"
|
||||||
},
|
},
|
||||||
"freePlan": {
|
"freePlan": {
|
||||||
@ -789,7 +794,7 @@
|
|||||||
"title": "Pro",
|
"title": "Pro",
|
||||||
"description": "For small teams to manage projects and team knowledge",
|
"description": "For small teams to manage projects and team knowledge",
|
||||||
"price": "{}",
|
"price": "{}",
|
||||||
"priceInfo": "/user per month billed annually\n\n{} billed monthly"
|
"priceInfo": "per user per month \nbilled annually\n\n{} billed monthly"
|
||||||
},
|
},
|
||||||
"planLabels": {
|
"planLabels": {
|
||||||
"itemOne": "Workspaces",
|
"itemOne": "Workspaces",
|
||||||
@ -830,6 +835,43 @@
|
|||||||
"downgradeLabel": "Downgrade plan"
|
"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": {
|
"common": {
|
||||||
"reset": "Reset"
|
"reset": "Reset"
|
||||||
},
|
},
|
||||||
@ -1441,7 +1483,7 @@
|
|||||||
"image": {
|
"image": {
|
||||||
"copiedToPasteBoard": "The image link has been copied to the clipboard",
|
"copiedToPasteBoard": "The image link has been copied to the clipboard",
|
||||||
"addAnImage": "Add an image",
|
"addAnImage": "Add an image",
|
||||||
"imageUploadFailed": "Image upload failed",
|
"imageUploadFailed": "Upload failed",
|
||||||
"errorCode": "Error code"
|
"errorCode": "Error code"
|
||||||
},
|
},
|
||||||
"math": {
|
"math": {
|
||||||
|
@ -148,7 +148,12 @@ impl Chat {
|
|||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("[Chat] failed to stream answer: {}", 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 _ = text_sink.send(format!("error:{}", err)).await;
|
||||||
|
}
|
||||||
|
|
||||||
let pb = ChatMessageErrorPB {
|
let pb = ChatMessageErrorPB {
|
||||||
chat_id: chat_id.clone(),
|
chat_id: chat_id.clone(),
|
||||||
error_message: err.to_string(),
|
error_message: err.to_string(),
|
||||||
|
Loading…
Reference in New Issue
Block a user