feat: ai billing (#5741)

* feat: start on AI plan+billing UI

* chore: enable plan and billing

* feat: cache workspace subscription + minor fixes (#5705)

* feat: update api from billing

* feat: add api for workspace subscription info (#5717)

* feat: refactor and start integrating AI plans

* feat: refine UI and add business logic for AI

* feat: complete UIUX for AI and limits

* chore: remove resolved todo

* chore: localize remove addon dialog

* chore: fix spacing issue for usage

* fix: interpret subscription + usage on action

* chore: update api for billing (#5735)

* chore: update revisions

* fix: remove subscription cache

* fix: copy improvements + use consistent dialog

* chore: update to the latest client api

* feat: support updating billing period

* Feat/ai billing cancel reason (#5752)

* chore: add cancellation reason field

* fix: ci add one retry for concurrent sign up

* chore: merge with main

* chore: half merge

* chore: fix conflict

* chore: observer error

* chore: remove unneeded protobuf and remove unwrap

* feat: added subscription plan details

* chore: check error code and update sidebar toast

* chore: periodically check billing state

* chore: editor ai error

* chore: return file upload error

* chore: fmt

* chore: clippy

* chore: disable upload image when exceed storage limitation

* chore: remove todo

* chore: remove openai i18n

* chore: update log

* chore: update client-api to fix stream error

* chore: clippy

* chore: fix language file

* chore: disable billing UI

---------

Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg>
Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Mathias Mogensen 2024-07-22 09:43:48 +02:00 committed by GitHub
parent 864768b3ba
commit 620e027c3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
121 changed files with 4141 additions and 1422 deletions

View File

@ -136,7 +136,7 @@ class DocumentService {
}) async {
final workspace = await FolderEventReadCurrentWorkspace().send();
return workspace.fold((l) async {
final payload = UploadedFilePB(
final payload = DownloadFilePB(
url: url,
);
final result = await DocumentEventDownloadFile(payload).send();

View File

@ -7,6 +7,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_extension.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:path/path.dart' as p;
@ -63,6 +64,12 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
);
return (s.url, null);
},
(e) => (null, e.msg),
(err) {
if (err.code == ErrorCode.FileStorageLimitExceeded) {
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
} else {
return (null, err.msg);
}
},
);
}

View File

@ -1,14 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'error.freezed.dart';
part 'error.g.dart';
@freezed
class AIError with _$AIError {
const factory AIError({
String? code,
required String message,
@Default(AIErrorCode.other) AIErrorCode code,
}) = _AIError;
factory AIError.fromJson(Map<String, Object?> json) =>
_$AIErrorFromJson(json);
}
enum AIErrorCode {
@JsonValue('AIResponseLimitExceeded')
aiResponseLimitExceeded,
@JsonValue('Other')
other,
}
extension AIErrorExtension on AIError {
bool get isLimitExceeded => code == AIErrorCode.aiResponseLimitExceeded;
}

View File

@ -0,0 +1,35 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class _AILimitDialog extends StatelessWidget {
const _AILimitDialog({
required this.message,
required this.onOkPressed,
});
final VoidCallback onOkPressed;
final String message;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: message,
okTitle: LocaleKeys.button_ok.tr(),
onOkPressed: onOkPressed,
titleUpperCase: false,
);
}
}
void showAILimitDialog(BuildContext context, String message) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) => _AILimitDialog(
message: message,
onOkPressed: () {},
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/ai_service.dart';
@ -17,6 +18,8 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'ai_limit_dialog.dart';
class AutoCompletionBlockKeys {
const AutoCompletionBlockKeys._();
@ -225,11 +228,15 @@ class _AutoCompletionBlockComponentState
onError: (error) async {
barrierDialog?.dismiss();
if (mounted) {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
}
},
);
@ -304,11 +311,15 @@ class _AutoCompletionBlockComponentState
onEnd: () async {},
onError: (error) async {
if (mounted) {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
}
},
);

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/startup/startup.dart';
@ -16,6 +17,8 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'ai_limit_dialog.dart';
class SmartEditBlockKeys {
const SmartEditBlockKeys._();
@ -426,11 +429,15 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
});
},
onError: (error) async {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
if (error.isLimitExceeded) {
showAILimitDialog(context, error.message);
} else {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
}
await _onExit();
},
);

View File

@ -91,6 +91,7 @@ enum FeatureFlag {
bool get isOn {
if ([
FeatureFlag.planBilling,
// release this feature in version 0.6.1
FeatureFlag.spaceDesign,
// release this feature in version 0.5.9
@ -110,6 +111,7 @@ enum FeatureFlag {
}
switch (this) {
case FeatureFlag.planBilling:
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
@ -117,7 +119,6 @@ enum FeatureFlag {
return true;
case FeatureFlag.collaborativeWorkspace:
case FeatureFlag.membersSettings:
case FeatureFlag.planBilling:
case FeatureFlag.unknown:
return false;
}

View File

@ -95,7 +95,9 @@ class AppFlowyCloudDeepLink {
}
if (_isPaymentSuccessUri(uri)) {
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess();
Log.debug("Payment success deep link: ${uri.toString()}");
final plan = uri.queryParameters['plan'];
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);
}
return _isAuthCallbackDeepLink(uri).fold(

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/wid
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
class AppFlowyAIService implements AIRepository {
@ -85,6 +87,15 @@ class CompletionStream {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
if (event == "AI_RESPONSE_LIMIT") {
onError(
AIError(
message: LocaleKeys.sideBar_aiResponseLitmit.tr(),
code: AIErrorCode.aiResponseLimitExceeded,
),
);
}
if (event.startsWith("start:")) {
await onStart();
}
@ -96,6 +107,7 @@ class CompletionStream {
if (event.startsWith("finish:")) {
await onEnd();
}
if (event.startsWith("error:")) {
onError(AIError(message: event.substring(6)));
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
@ -10,7 +11,10 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';
abstract class IUserBackendService {
Future<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
SubscriptionPlanPB plan,
);
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
String workspaceId,
SubscriptionPlanPB plan,
@ -228,9 +232,10 @@ class UserBackendService implements IUserBackendService {
return UserEventLeaveWorkspace(data).send();
}
static Future<FlowyResult<RepeatedWorkspaceSubscriptionPB, FlowyError>>
getWorkspaceSubscriptions() {
return UserEventGetWorkspaceSubscriptions().send();
static Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
getWorkspaceSubscriptionInfo(String workspaceId) {
final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
return UserEventGetWorkspaceSubscriptionInfo(params).send();
}
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
@ -250,15 +255,32 @@ class UserBackendService implements IUserBackendService {
..recurringInterval = RecurringIntervalPB.Year
..workspaceSubscriptionPlan = plan
..successUrl =
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success?plan=${plan.toRecognizable()}';
return UserEventSubscribeWorkspace(request).send();
}
@override
Future<FlowyResult<void, FlowyError>> cancelSubscription(
String workspaceId,
SubscriptionPlanPB plan,
) {
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;
final request = CancelWorkspaceSubscriptionPB()
..workspaceId = workspaceId
..plan = plan;
return UserEventCancelWorkspaceSubscription(request).send();
}
Future<FlowyResult<void, FlowyError>> updateSubscriptionPeriod(
String workspaceId,
SubscriptionPlanPB plan,
RecurringIntervalPB interval,
) {
final request = UpdateWorkspaceSubscriptionPaymentPeriodPB()
..workspaceId = workspaceId
..plan = plan
..recurringInterval = interval;
return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send();
}
}

View File

@ -1,15 +1,24 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'settings_billing_bloc.freezed.dart';
@ -17,86 +26,273 @@ class SettingsBillingBloc
extends Bloc<SettingsBillingEvent, SettingsBillingState> {
SettingsBillingBloc({
required this.workspaceId,
required Int64 userId,
}) : super(const _Initial()) {
_userService = UserBackendService(userId: userId);
_service = WorkspaceService(workspaceId: workspaceId);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
on<SettingsBillingEvent>((event, emit) async {
await event.when(
started: () async {
emit(const SettingsBillingState.loading());
final snapshots = await Future.wait([
UserBackendService.getWorkspaceSubscriptions(),
_service.getBillingPortal(),
]);
FlowyError? error;
final subscription = snapshots.first.fold(
(s) =>
(s as RepeatedWorkspaceSubscriptionPB)
.items
.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
WorkspaceSubscriptionPB(
workspaceId: workspaceId,
subscriptionPlan: SubscriptionPlanPB.None,
isActive: true,
),
(e) {
// Not a Cjstomer yet
if (e.code == ErrorCode.InvalidParams) {
return WorkspaceSubscriptionPB(
workspaceId: workspaceId,
subscriptionPlan: SubscriptionPlanPB.None,
isActive: true,
);
}
final result = await UserBackendService.getWorkspaceSubscriptionInfo(
workspaceId,
);
final subscriptionInfo = result.fold(
(s) => s,
(e) {
error = e;
return null;
},
);
final billingPortalResult = snapshots.last;
final billingPortal = billingPortalResult.fold(
(s) => s as BillingPortalPB,
(e) {
// Not a customer yet
if (e.code == ErrorCode.InvalidParams) {
return BillingPortalPB();
}
error = e;
return null;
},
);
if (subscription == null || billingPortal == null || error != null) {
if (subscriptionInfo == null || error != null) {
return emit(SettingsBillingState.error(error: error));
}
if (!_billingPortalCompleter.isCompleted) {
unawaited(_fetchBillingPortal());
unawaited(
_billingPortalCompleter.future.then(
(result) {
if (isClosed) return;
result.fold(
(portal) {
_billingPortal = portal;
add(
SettingsBillingEvent.billingPortalFetched(
billingPortal: portal,
),
);
},
(e) => Log.error('Error fetching billing portal: $e'),
);
},
),
);
}
emit(
SettingsBillingState.ready(
subscription: subscription,
billingPortal: billingPortal,
subscriptionInfo: subscriptionInfo,
billingPortal: _billingPortal,
),
);
},
billingPortalFetched: (billingPortal) async => state.maybeWhen(
orElse: () {},
ready: (subscriptionInfo, _, plan, isLoading) => emit(
SettingsBillingState.ready(
subscriptionInfo: subscriptionInfo,
billingPortal: billingPortal,
successfulPlanUpgrade: plan,
isLoading: isLoading,
),
),
),
openCustomerPortal: () async {
if (_billingPortalCompleter.isCompleted && _billingPortal != null) {
return afLaunchUrlString(_billingPortal!.url);
}
await _billingPortalCompleter.future;
if (_billingPortal != null) {
await afLaunchUrlString(_billingPortal!.url);
}
},
addSubscription: (plan) async {
final result =
await _userService.createSubscription(workspaceId, plan);
result.fold(
(link) => afLaunchUrlString(link.paymentLink),
(f) => Log.error(f.msg, f),
);
},
cancelSubscription: (plan) async {
final s = state.mapOrNull(ready: (s) => s);
if (s == null) {
return;
}
emit(s.copyWith(isLoading: true));
final result =
await _userService.cancelSubscription(workspaceId, plan);
final successOrNull = result.fold(
(_) => true,
(f) {
Log.error(
'Failed to cancel subscription of ${plan.label}: ${f.msg}',
f,
);
return null;
},
);
if (successOrNull != true) {
return;
}
final subscriptionInfo = state.mapOrNull(
ready: (s) => s.subscriptionInfo,
);
// This is impossible, but for good measure
if (subscriptionInfo == null) {
return;
}
subscriptionInfo.freeze();
final newInfo = subscriptionInfo.rebuild((value) {
if (plan.isAddOn) {
value.addOns.removeWhere(
(addon) => addon.addOnSubscription.subscriptionPlan == plan,
);
}
if (plan == WorkspacePlanPB.ProPlan &&
value.plan == WorkspacePlanPB.ProPlan) {
value.plan = WorkspacePlanPB.FreePlan;
value.planSubscription.freeze();
value.planSubscription = value.planSubscription.rebuild((sub) {
sub.status = WorkspaceSubscriptionStatusPB.Active;
sub.subscriptionPlan = SubscriptionPlanPB.Free;
});
}
});
emit(
SettingsBillingState.ready(
subscriptionInfo: newInfo,
billingPortal: _billingPortal,
),
);
},
paymentSuccessful: (plan) async {
final result = await UserBackendService.getWorkspaceSubscriptionInfo(
workspaceId,
);
final subscriptionInfo = result.toNullable();
if (subscriptionInfo != null) {
emit(
SettingsBillingState.ready(
subscriptionInfo: subscriptionInfo,
billingPortal: _billingPortal,
),
);
}
},
updatePeriod: (plan, interval) async {
final s = state.mapOrNull(ready: (s) => s);
if (s == null) {
return;
}
emit(s.copyWith(isLoading: true));
final result = await _userService.updateSubscriptionPeriod(
workspaceId,
plan,
interval,
);
final successOrNull = result.fold((_) => true, (f) {
Log.error(
'Failed to update subscription period of ${plan.label}: ${f.msg}',
f,
);
return null;
});
if (successOrNull != true) {
return emit(s.copyWith(isLoading: false));
}
// Fetch new subscription info
final newResult =
await UserBackendService.getWorkspaceSubscriptionInfo(
workspaceId,
);
final newSubscriptionInfo = newResult.toNullable();
if (newSubscriptionInfo != null) {
emit(
SettingsBillingState.ready(
subscriptionInfo: newSubscriptionInfo,
billingPortal: _billingPortal,
),
);
}
},
);
});
}
late final String workspaceId;
late final WorkspaceService _service;
late final UserBackendService _userService;
final _billingPortalCompleter =
Completer<FlowyResult<BillingPortalPB, FlowyError>>();
BillingPortalPB? _billingPortal;
late final SubscriptionSuccessListenable _successListenable;
@override
Future<void> close() {
_successListenable.removeListener(_onPaymentSuccessful);
return super.close();
}
Future<void> _fetchBillingPortal() async {
final billingPortalResult = await _service.getBillingPortal();
_billingPortalCompleter.complete(billingPortalResult);
}
Future<void> _onPaymentSuccessful() async => add(
SettingsBillingEvent.paymentSuccessful(
plan: _successListenable.subscribedPlan,
),
);
}
@freezed
class SettingsBillingEvent with _$SettingsBillingEvent {
const factory SettingsBillingEvent.started() = _Started;
const factory SettingsBillingEvent.billingPortalFetched({
required BillingPortalPB billingPortal,
}) = _BillingPortalFetched;
const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal;
const factory SettingsBillingEvent.addSubscription(SubscriptionPlanPB plan) =
_AddSubscription;
const factory SettingsBillingEvent.cancelSubscription(
SubscriptionPlanPB plan,
) = _CancelSubscription;
const factory SettingsBillingEvent.paymentSuccessful({
SubscriptionPlanPB? plan,
}) = _PaymentSuccessful;
const factory SettingsBillingEvent.updatePeriod({
required SubscriptionPlanPB plan,
required RecurringIntervalPB interval,
}) = _UpdatePeriod;
}
@freezed
class SettingsBillingState with _$SettingsBillingState {
class SettingsBillingState extends Equatable with _$SettingsBillingState {
const SettingsBillingState._();
const factory SettingsBillingState.initial() = _Initial;
const factory SettingsBillingState.loading() = _Loading;
@ -106,7 +302,22 @@ class SettingsBillingState with _$SettingsBillingState {
}) = _Error;
const factory SettingsBillingState.ready({
required WorkspaceSubscriptionPB subscription,
required WorkspaceSubscriptionInfoPB subscriptionInfo,
required BillingPortalPB? billingPortal,
@Default(null) SubscriptionPlanPB? successfulPlanUpgrade,
@Default(false) bool isLoading,
}) = _Ready;
@override
List<Object?> get props => maybeWhen(
orElse: () => const [],
error: (error) => [error],
ready: (subscription, billingPortal, plan, isLoading) => [
subscription,
billingPortal,
plan,
isLoading,
...subscription.addOns,
],
);
}

View File

@ -0,0 +1,65 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/notification_helper.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-storage/notification.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
class StoregeNotificationParser
extends NotificationParser<StorageNotification, FlowyError> {
StoregeNotificationParser({
super.id,
required super.callback,
}) : super(
tyParser: (ty, source) =>
source == "storage" ? StorageNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
class StoreageNotificationListener {
StoreageNotificationListener({
void Function(FlowyError error)? onError,
}) : _parser = StoregeNotificationParser(
callback: (
StorageNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
result.fold(
(data) {
try {
switch (ty) {
case StorageNotification.FileStorageLimitExceeded:
onError?.call(FlowyError.fromBuffer(data));
break;
}
} catch (e) {
Log.error(
"$StoreageNotificationListener deserialize PB fail",
e,
);
}
},
(err) {
Log.error("Error in StoreageNotificationListener", err);
},
);
},
) {
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
StoregeNotificationParser? _parser;
StreamSubscription<SubscribeObject>? _subscription;
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
_subscription = null;
}
}

View File

@ -3,18 +3,18 @@ import 'package:flutter/foundation.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'settings_plan_bloc.freezed.dart';
@ -30,13 +30,14 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
on<SettingsPlanEvent>((event, emit) async {
await event.when(
started: (withShowSuccessful) async {
emit(const SettingsPlanState.loading());
started: (withSuccessfulUpgrade, shouldLoad) async {
if (shouldLoad) {
emit(const SettingsPlanState.loading());
}
final snapshots = await Future.wait([
_service.getWorkspaceUsage(),
UserBackendService.getWorkspaceSubscriptions(),
_service.getBillingPortal(),
UserBackendService.getWorkspaceSubscriptionInfo(workspaceId),
]);
FlowyError? error;
@ -49,39 +50,16 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
},
);
final subscription = snapshots[1].fold(
(s) =>
(s as RepeatedWorkspaceSubscriptionPB)
.items
.firstWhereOrNull((i) => i.workspaceId == workspaceId) ??
WorkspaceSubscriptionPB(
workspaceId: workspaceId,
subscriptionPlan: SubscriptionPlanPB.None,
isActive: true,
),
final subscriptionInfo = snapshots[1].fold(
(s) => s as WorkspaceSubscriptionInfoPB,
(f) {
error = f;
return null;
},
);
final billingPortalResult = snapshots.last;
final billingPortal = billingPortalResult.fold(
(s) => s as BillingPortalPB,
(e) {
// Not a customer yet
if (e.code == ErrorCode.InvalidParams) {
return BillingPortalPB();
}
error = e;
return null;
},
);
if (usageResult == null ||
subscription == null ||
billingPortal == null ||
subscriptionInfo == null ||
error != null) {
return emit(SettingsPlanState.error(error: error));
}
@ -89,18 +67,16 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
emit(
SettingsPlanState.ready(
workspaceUsage: usageResult,
subscription: subscription,
billingPortal: billingPortal,
showSuccessDialog: withShowSuccessful,
subscriptionInfo: subscriptionInfo,
successfulPlanUpgrade: withSuccessfulUpgrade,
),
);
if (withShowSuccessful) {
if (withSuccessfulUpgrade != null) {
emit(
SettingsPlanState.ready(
workspaceUsage: usageResult,
subscription: subscription,
billingPortal: billingPortal,
subscriptionInfo: subscriptionInfo,
),
);
}
@ -108,12 +84,15 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
addSubscription: (plan) async {
final result = await _userService.createSubscription(
workspaceId,
SubscriptionPlanPB.Pro,
plan,
);
result.fold(
(pl) => afLaunchUrlString(pl.paymentLink),
(f) => Log.error(f.msg, f),
(f) => Log.error(
'Failed to fetch paymentlink for $plan: ${f.msg}',
f,
),
);
},
cancelSubscription: () async {
@ -121,16 +100,79 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
.mapOrNull(ready: (state) => state)
?.copyWith(downgradeProcessing: true);
emit(newState ?? state);
await _userService.cancelSubscription(workspaceId);
add(const SettingsPlanEvent.started());
// We can hardcode the subscription plan here because we cannot cancel addons
// on the Plan page
final result = await _userService.cancelSubscription(
workspaceId,
SubscriptionPlanPB.Pro,
);
final successOrNull = result.fold(
(_) => true,
(f) {
Log.error('Failed to cancel subscription of Pro: ${f.msg}', f);
return null;
},
);
if (successOrNull != true) {
return;
}
final subscriptionInfo = state.mapOrNull(
ready: (s) => s.subscriptionInfo,
);
// This is impossible, but for good measure
if (subscriptionInfo == null) {
return;
}
// We assume their new plan is Free, since we only have Pro plan
// at the moment.
subscriptionInfo.freeze();
final newInfo = subscriptionInfo.rebuild((value) {
value.plan = WorkspacePlanPB.FreePlan;
value.planSubscription.freeze();
value.planSubscription = value.planSubscription.rebuild((sub) {
sub.status = WorkspaceSubscriptionStatusPB.Active;
sub.subscriptionPlan = SubscriptionPlanPB.Free;
});
});
// We need to remove unlimited indicator for storage and
// AI usage, if they don't have an addon that changes this behavior.
final usage = state.mapOrNull(ready: (s) => s.workspaceUsage)!;
usage.freeze();
final newUsage = usage.rebuild((value) {
if (!newInfo.hasAIMax && !newInfo.hasAIOnDevice) {
value.aiResponsesUnlimited = false;
}
value.storageBytesUnlimited = false;
});
emit(
SettingsPlanState.ready(
subscriptionInfo: newInfo,
workspaceUsage: newUsage,
),
);
},
paymentSuccessful: () {
paymentSuccessful: (plan) {
final readyState = state.mapOrNull(ready: (state) => state);
if (readyState == null) {
return;
}
add(const SettingsPlanEvent.started(withShowSuccessful: true));
add(
SettingsPlanEvent.started(
withSuccessfulUpgrade: plan,
shouldLoad: false,
),
);
},
);
});
@ -141,9 +183,11 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;
void _onPaymentSuccessful() {
add(const SettingsPlanEvent.paymentSuccessful());
}
Future<void> _onPaymentSuccessful() async => add(
SettingsPlanEvent.paymentSuccessful(
plan: _successListenable.subscribedPlan,
),
);
@override
Future<void> close() async {
@ -155,12 +199,18 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
@freezed
class SettingsPlanEvent with _$SettingsPlanEvent {
const factory SettingsPlanEvent.started({
@Default(false) bool withShowSuccessful,
@Default(null) SubscriptionPlanPB? withSuccessfulUpgrade,
@Default(true) bool shouldLoad,
}) = _Started;
const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =
_AddSubscription;
const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription;
const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful;
const factory SettingsPlanEvent.paymentSuccessful({
@Default(null) SubscriptionPlanPB? plan,
}) = _PaymentSuccessful;
}
@freezed
@ -175,9 +225,8 @@ class SettingsPlanState with _$SettingsPlanState {
const factory SettingsPlanState.ready({
required WorkspaceUsagePB workspaceUsage,
required WorkspaceSubscriptionPB subscription,
required BillingPortalPB? billingPortal,
@Default(false) bool showSuccessDialog,
required WorkspaceSubscriptionInfoPB subscriptionInfo,
@Default(null) SubscriptionPlanPB? successfulPlanUpgrade,
@Default(false) bool downgradeProcessing,
}) = _Ready;
}

View File

@ -1,26 +1,115 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';
import 'package:easy_localization/easy_localization.dart';
extension SubscriptionLabels on WorkspaceSubscriptionPB {
String get label => switch (subscriptionPlan) {
SubscriptionPlanPB.None =>
extension SubscriptionLabels on WorkspaceSubscriptionInfoPB {
String get label => switch (plan) {
WorkspacePlanPB.FreePlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
WorkspacePlanPB.ProPlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),
WorkspacePlanPB.TeamPlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
_ => 'N/A',
};
String get info => switch (plan) {
WorkspacePlanPB.FreePlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),
WorkspacePlanPB.ProPlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),
WorkspacePlanPB.TeamPlan =>
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
_ => 'N/A',
};
}
extension AllSubscriptionLabels on SubscriptionPlanPB {
String get label => switch (this) {
SubscriptionPlanPB.Free =>
LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),
SubscriptionPlanPB.Pro =>
LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),
SubscriptionPlanPB.Team =>
LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),
_ => 'N/A',
};
String get info => switch (subscriptionPlan) {
SubscriptionPlanPB.None =>
LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),
SubscriptionPlanPB.Pro =>
LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),
SubscriptionPlanPB.Team =>
LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),
SubscriptionPlanPB.AiMax =>
LocaleKeys.settings_billingPage_addons_aiMax_label.tr(),
SubscriptionPlanPB.AiLocal =>
LocaleKeys.settings_billingPage_addons_aiOnDevice_label.tr(),
_ => 'N/A',
};
}
extension WorkspaceSubscriptionStatusExt on WorkspaceSubscriptionInfoPB {
bool get isCanceled =>
planSubscription.status == WorkspaceSubscriptionStatusPB.Canceled;
}
extension WorkspaceAddonsExt on WorkspaceSubscriptionInfoPB {
bool get hasAIMax =>
addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiMax);
bool get hasAIOnDevice =>
addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiLocal);
}
/// These have to match [SubscriptionSuccessListenable.subscribedPlan] labels
extension ToRecognizable on SubscriptionPlanPB {
String? toRecognizable() => switch (this) {
SubscriptionPlanPB.Free => 'free',
SubscriptionPlanPB.Pro => 'pro',
SubscriptionPlanPB.Team => 'team',
SubscriptionPlanPB.AiMax => 'ai_max',
SubscriptionPlanPB.AiLocal => 'ai_local',
_ => null,
};
}
extension PlanHelper on SubscriptionPlanPB {
/// Returns true if the plan is an add-on and not
/// a workspace plan.
///
bool get isAddOn => switch (this) {
SubscriptionPlanPB.AiMax => true,
SubscriptionPlanPB.AiLocal => true,
_ => false,
};
String get priceMonthBilling => switch (this) {
SubscriptionPlanPB.Free => 'US\$0',
SubscriptionPlanPB.Pro => 'US\$12.5',
SubscriptionPlanPB.Team => 'US\$15',
SubscriptionPlanPB.AiMax => 'US\$10',
SubscriptionPlanPB.AiLocal => 'US\$10',
_ => 'US\$0',
};
String get priceAnnualBilling => switch (this) {
SubscriptionPlanPB.Free => 'US\$0',
SubscriptionPlanPB.Pro => 'US\$10',
SubscriptionPlanPB.Team => 'US\$12.5',
SubscriptionPlanPB.AiMax => 'US\$8',
SubscriptionPlanPB.AiLocal => 'US\$8',
_ => 'US\$0',
};
}
extension IntervalLabel on RecurringIntervalPB {
String get label => switch (this) {
RecurringIntervalPB.Month =>
LocaleKeys.settings_billingPage_monthlyInterval.tr(),
RecurringIntervalPB.Year =>
LocaleKeys.settings_billingPage_annualInterval.tr(),
_ => LocaleKeys.settings_billingPage_monthlyInterval.tr(),
};
String get priceInfo => switch (this) {
RecurringIntervalPB.Month =>
LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(),
RecurringIntervalPB.Year =>
LocaleKeys.settings_billingPage_annualPriceInfo.tr(),
_ => LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(),
};
}

View File

@ -6,8 +6,13 @@ final _storageNumberFormat = NumberFormat()
..minimumFractionDigits = 0;
extension PresentableUsage on WorkspaceUsagePB {
String get totalBlobInGb =>
(totalBlobBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString();
String get totalBlobInGb {
if (storageBytesLimit == 0) {
return '0';
}
return _storageNumberFormat
.format(storageBytesLimit.toInt() / (1024 * 1024 * 1024));
}
/// We use [NumberFormat] to format the current blob in GB.
///
@ -16,5 +21,5 @@ extension PresentableUsage on WorkspaceUsagePB {
/// And [NumberFormat.minimumFractionDigits] is set to 0.
///
String get currentBlobInGb =>
_storageNumberFormat.format(totalBlobBytes.toInt() / 1024 / 1024 / 1024);
_storageNumberFormat.format(storageBytes.toInt() / 1024 / 1024 / 1024);
}

View File

@ -26,9 +26,11 @@ enum SettingsPage {
class SettingsDialogBloc
extends Bloc<SettingsDialogEvent, SettingsDialogState> {
SettingsDialogBloc(this.userProfile)
: _userListener = UserListener(userProfile: userProfile),
super(SettingsDialogState.initial(userProfile)) {
SettingsDialogBloc(
this.userProfile, {
SettingsPage? initPage,
}) : _userListener = UserListener(userProfile: userProfile),
super(SettingsDialogState.initial(userProfile, initPage)) {
_dispatch();
}
@ -87,9 +89,12 @@ class SettingsDialogState with _$SettingsDialogState {
required SettingsPage page,
}) = _SettingsDialogState;
factory SettingsDialogState.initial(UserProfilePB userProfile) =>
factory SettingsDialogState.initial(
UserProfilePB userProfile,
SettingsPage? page,
) =>
SettingsDialogState(
userProfile: userProfile,
page: SettingsPage.account,
page: page ?? SettingsPage.account,
);
}

View File

@ -0,0 +1,206 @@
import 'dart:async';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/file_storage/file_storage_listener.dart';
import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/dispatch/error.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'sidebar_plan_bloc.freezed.dart';
class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
SidebarPlanBloc() : super(const SidebarPlanState()) {
// After user pays for the subscription, the subscription success listenable will be triggered
final subscriptionListener = getIt<SubscriptionSuccessListenable>();
subscriptionListener.addListener(() {
final plan = subscriptionListener.subscribedPlan;
Log.info("Subscription success listenable triggered: $plan");
if (!isClosed) {
// Notify the user that they have switched to a new plan. It would be better if we use websocket to
// notify the client when plan switching.
if (state.workspaceId != null) {
final payload = SuccessWorkspaceSubscriptionPB(
workspaceId: state.workspaceId,
);
if (plan != null) {
payload.plan = plan;
}
UserEventNotifyDidSwitchPlan(payload).send().then((result) {
result.fold(
// After the user has switched to a new plan, we need to refresh the workspace usage.
(_) => _checkWorkspaceUsage(),
(error) => Log.error("NotifyDidSwitchPlan failed: $error"),
);
});
} else {
Log.error(
"Unexpected empty workspace id when subscription success listenable triggered. It should not happen. If happens, it must be a bug",
);
}
}
});
_storageListener = StoreageNotificationListener(
onError: (error) {
if (!isClosed) {
add(SidebarPlanEvent.receiveError(error));
}
},
);
_globalErrorListener = GlobalErrorCodeNotifier.add(
onError: (error) {
if (!isClosed) {
add(SidebarPlanEvent.receiveError(error));
}
},
onErrorIf: (error) {
const relevantErrorCodes = {
ErrorCode.AIResponseLimitExceeded,
ErrorCode.FileStorageLimitExceeded,
};
return relevantErrorCodes.contains(error.code);
},
);
on<SidebarPlanEvent>(_handleEvent);
}
Future<void> dispose() async {
if (_globalErrorListener != null) {
GlobalErrorCodeNotifier.remove(_globalErrorListener!);
}
await _storageListener?.stop();
_storageListener = null;
}
ErrorListener? _globalErrorListener;
StoreageNotificationListener? _storageListener;
Future<void> _handleEvent(
SidebarPlanEvent event,
Emitter<SidebarPlanState> emit,
) async {
await event.when(
receiveError: (FlowyError error) async {
emit(
state.copyWith(
tierIndicator: const SidebarToastTierIndicator.storageLimitHit(),
),
);
},
init: (String workspaceId, UserProfilePB userProfile) {
emit(
state.copyWith(
workspaceId: workspaceId,
userProfile: userProfile,
),
);
_checkWorkspaceUsage();
},
updateWorkspaceUsage: (WorkspaceUsagePB usage) {
// when the user's storage bytes are limited, show the upgrade tier button
if (!usage.storageBytesUnlimited) {
if (usage.storageBytes >= usage.storageBytesLimit) {
add(
const SidebarPlanEvent.updateTierIndicator(
SidebarToastTierIndicator.storageLimitHit(),
),
);
/// Checks if the user needs to upgrade to the Pro Plan.
/// If the user needs to upgrade, it means they don't need to enable the AI max tier.
/// This function simply returns without performing any further actions.
return;
}
}
// when user's AI responses are limited, show the AI max tier button.
if (!usage.aiResponsesUnlimited) {
if (usage.aiResponsesCount >= usage.aiResponsesCountLimit) {
add(
const SidebarPlanEvent.updateTierIndicator(
SidebarToastTierIndicator.aiMaxiLimitHit(),
),
);
return;
}
}
// hide the tier indicator
add(
const SidebarPlanEvent.updateTierIndicator(
SidebarToastTierIndicator.loading(),
),
);
},
updateTierIndicator: (SidebarToastTierIndicator indicator) {
emit(
state.copyWith(
tierIndicator: indicator,
),
);
},
);
}
void _checkWorkspaceUsage() {
if (state.workspaceId != null) {
final payload = UserWorkspaceIdPB(workspaceId: state.workspaceId!);
UserEventGetWorkspaceUsage(payload).send().then((result) {
result.fold(
(usage) {
add(SidebarPlanEvent.updateWorkspaceUsage(usage));
},
(error) {
Log.error("Failed to get workspace usage, error: $error");
},
);
});
}
}
}
@freezed
class SidebarPlanEvent with _$SidebarPlanEvent {
const factory SidebarPlanEvent.init(
String workspaceId,
UserProfilePB userProfile,
) = _Init;
const factory SidebarPlanEvent.updateWorkspaceUsage(
WorkspaceUsagePB usage,
) = _UpdateWorkspaceUsage;
const factory SidebarPlanEvent.updateTierIndicator(
SidebarToastTierIndicator indicator,
) = _UpdateTierIndicator;
const factory SidebarPlanEvent.receiveError(FlowyError error) = _ReceiveError;
}
@freezed
class SidebarPlanState with _$SidebarPlanState {
const factory SidebarPlanState({
FlowyError? error,
UserProfilePB? userProfile,
String? workspaceId,
WorkspaceUsagePB? usage,
@Default(SidebarToastTierIndicator.loading())
SidebarToastTierIndicator tierIndicator,
}) = _SidebarPlanState;
}
@freezed
class SidebarToastTierIndicator with _$SidebarToastTierIndicator {
// when start downloading the model
const factory SidebarToastTierIndicator.storageLimitHit() = _StorageLimitHit;
const factory SidebarToastTierIndicator.aiMaxiLimitHit() = _aiMaxLimitHit;
const factory SidebarToastTierIndicator.loading() = _Loading;
}

View File

@ -1,7 +1,25 @@
import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
class SubscriptionSuccessListenable extends ChangeNotifier {
SubscriptionSuccessListenable();
void onPaymentSuccess() => notifyListeners();
String? _plan;
SubscriptionPlanPB? get subscribedPlan => switch (_plan) {
'free' => SubscriptionPlanPB.Free,
'pro' => SubscriptionPlanPB.Pro,
'team' => SubscriptionPlanPB.Team,
'ai_max' => SubscriptionPlanPB.AiMax,
'ai_local' => SubscriptionPlanPB.AiLocal,
_ => null,
};
void onPaymentSuccess(String? plan) {
Log.info("Payment success: $plan");
_plan = plan;
notifyListeners();
}
}

View File

@ -0,0 +1,195 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarToast extends StatefulWidget {
const SidebarToast({super.key});
@override
State<SidebarToast> createState() => _SidebarToastState();
}
class _SidebarToastState extends State<SidebarToast> {
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarPlanBloc, SidebarPlanState>(
listener: (context, state) {
// Show a dialog when the user hits the storage limit, After user click ok, it will navigate to the plan page.
// Even though the dislog is dissmissed, if the user triggers the storage limit again, the dialog will show again.
state.tierIndicator.maybeWhen(
storageLimitHit: () {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _showStorageLimitDialog(context),
debugLabel: 'Sidebar.showStorageLimit',
);
},
orElse: () {
// Do nothing
},
);
},
builder: (context, state) {
return BlocBuilder<SidebarPlanBloc, SidebarPlanState>(
builder: (context, state) {
return state.tierIndicator.when(
storageLimitHit: () => Column(
children: [
const Divider(height: 0.6),
PlanIndicator(
planName: "Pro",
text: LocaleKeys.sideBar_upgradeToPro.tr(),
onTap: () {
_hanldeOnTap(context, SubscriptionPlanPB.Pro);
},
reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
),
],
),
aiMaxiLimitHit: () => Column(
children: [
const Divider(height: 0.6),
PlanIndicator(
planName: "AI Max",
text: LocaleKeys.sideBar_upgradeToAIMax.tr(),
onTap: () {
_hanldeOnTap(context, SubscriptionPlanPB.AiMax);
},
reason: LocaleKeys.sideBar_aiResponseLitmitDialogTitle.tr(),
),
],
),
loading: () {
return const SizedBox.shrink();
},
);
},
);
},
);
}
void _showStorageLimitDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) => _StorageLimitDialog(
onOkPressed: () {
final userProfile = context.read<SidebarPlanBloc>().state.userProfile;
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
if (userProfile != null) {
showSettingsDialog(
context,
userProfile,
userWorkspaceBloc,
SettingsPage.plan,
);
} else {
Log.error(
"UserProfile is null. It should not happen. If you see this error, it's a bug.",
);
}
},
),
);
}
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 {
const PlanIndicator({
required this.planName,
required this.text,
required this.onTap,
required this.reason,
super.key,
});
final String planName;
final String reason;
final String text;
final Function() onTap;
final textColor = const Color(0xFFE8E2EE);
final secondaryColor = const Color(0xFF653E8C);
@override
Widget build(BuildContext context) {
return Column(
children: [
FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
text: FlowyText(
text,
color: textColor,
fontSize: 12,
),
radius: BorderRadius.zero,
leftIconSize: const Size(40, 20),
leftIcon: Badge(
padding: const EdgeInsets.symmetric(horizontal: 6),
backgroundColor: secondaryColor,
label: FlowyText.semibold(
planName,
fontSize: 12,
color: textColor,
),
),
onTap: onTap,
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 6),
child: Opacity(
opacity: 0.4,
child: FlowyText(
reason,
textAlign: TextAlign.start,
color: textColor,
fontSize: 8,
maxLines: 10,
),
),
),
],
);
}
}
class _StorageLimitDialog extends StatelessWidget {
const _StorageLimitDialog({
required this.onOkPressed,
});
final VoidCallback onOkPressed;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),
okTitle: LocaleKeys.sideBar_purchaseStorageSpace.tr(),
onOkPressed: onOkPressed,
titleUpperCase: false,
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -93,6 +94,7 @@ void showSettingsDialog(
BuildContext context,
UserProfilePB userProfile, [
UserWorkspaceBloc? bloc,
SettingsPage? initPage,
]) {
AFFocusManager.of(context).notifyLoseFocus();
showDialog(
@ -107,6 +109,7 @@ void showSettingsDialog(
],
child: SettingsDialog(
userProfile,
initPage: initPage,
didLogout: () async {
// Pop the dialog using the dialog context
Navigator.of(dialogContext).pop();

View File

@ -13,6 +13,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/favorite/prelude.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@ -100,6 +101,9 @@ class HomeSideBar extends StatelessWidget {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
final workspaceId =
state.currentWorkspace?.workspaceId ?? workspaceSetting.workspaceId;
return MultiBlocProvider(
providers: [
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
@ -108,8 +112,7 @@ class HomeSideBar extends StatelessWidget {
..add(
SidebarSectionsEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
workspaceId,
),
),
),
@ -118,12 +121,15 @@ class HomeSideBar extends StatelessWidget {
..add(
SpaceEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
workspaceId,
openFirstPage: false,
),
),
),
BlocProvider(
create: (_) => SidebarPlanBloc()
..add(SidebarPlanEvent.init(workspaceId, userProfile)),
),
],
child: MultiBlocListener(
listeners: [
@ -357,6 +363,9 @@ class _SidebarState extends State<_Sidebar> {
child: const SidebarFooter(),
),
const VSpace(14),
// toast
// const SidebarToast(),
],
),
),

View File

@ -1,3 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
@ -18,9 +22,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SpacePermissionSwitch extends StatefulWidget {

View File

@ -151,7 +151,7 @@ class _ToggleLocalAIDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
title: LocaleKeys.settings_aiPage_keys_disableLocalAIDialog.tr(),
message: LocaleKeys.settings_aiPage_keys_disableLocalAIDialog.tr(),
okTitle: LocaleKeys.button_confirm.tr(),
cancelTitle: LocaleKeys.button_cancel.tr(),
onOkPressed: onOkPressed,

View File

@ -10,7 +10,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/prelude.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
@ -210,18 +209,19 @@ class SignInOutButton extends StatelessWidget {
if (signIn) {
_showSignInDialog(context);
} else {
SettingsAlertDialog(
showConfirmDialog(
context: context,
title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
subtitle: switch (userProfile.encryptionType) {
description: switch (userProfile.encryptionType) {
EncryptionTypePB.Symmetric =>
LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(),
_ => LocaleKeys.settings_menu_logoutPrompt.tr(),
},
confirm: () async {
onConfirm: () async {
await getIt<AuthService>().signOut();
onAction();
},
).show(context);
);
}
},
),

View File

@ -1,22 +1,34 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart';
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../generated/locale_keys.g.dart';
import '../../../../plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
class SettingsBillingView extends StatelessWidget {
const _buttonsMinWidth = 100.0;
class SettingsBillingView extends StatefulWidget {
const SettingsBillingView({
super.key,
required this.workspaceId,
@ -26,12 +38,34 @@ class SettingsBillingView extends StatelessWidget {
final String workspaceId;
final UserProfilePB user;
@override
State<SettingsBillingView> createState() => _SettingsBillingViewState();
}
class _SettingsBillingViewState extends State<SettingsBillingView> {
Loading? loadingIndicator;
RecurringIntervalPB? selectedInterval;
final ValueNotifier<bool> enablePlanChangeNotifier = ValueNotifier(false);
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsBillingBloc>(
create: (context) => SettingsBillingBloc(workspaceId: workspaceId)
..add(const SettingsBillingEvent.started()),
child: BlocBuilder<SettingsBillingBloc, SettingsBillingState>(
create: (_) => SettingsBillingBloc(
workspaceId: widget.workspaceId,
userId: widget.user.id,
)..add(const SettingsBillingEvent.started()),
child: BlocConsumer<SettingsBillingBloc, SettingsBillingState>(
listenWhen: (previous, current) =>
previous.mapOrNull(ready: (s) => s.isLoading) !=
current.mapOrNull(ready: (s) => s.isLoading),
listener: (context, state) {
if (state.mapOrNull(ready: (s) => s.isLoading) == true) {
loadingIndicator = Loading(context)..start();
} else {
loadingIndicator?.stop();
loadingIndicator = null;
}
},
builder: (context, state) {
return state.map(
initial: (_) => const SizedBox.shrink(),
@ -56,8 +90,8 @@ class SettingsBillingView extends StatelessWidget {
return ErrorWidget.withDetails(message: 'Something went wrong!');
},
ready: (state) {
final billingPortalEnabled = state.billingPortal != null &&
state.billingPortal!.url.isNotEmpty;
final billingPortalEnabled =
state.subscriptionInfo.plan != WorkspacePlanPB.FreePlan;
return SettingsBody(
title: LocaleKeys.settings_billingPage_title.tr(),
@ -68,27 +102,67 @@ class SettingsBillingView extends StatelessWidget {
SingleSettingAction(
onPressed: () => _openPricingDialog(
context,
workspaceId,
user.id,
state.subscription,
widget.workspaceId,
widget.user.id,
state.subscriptionInfo,
),
fontWeight: FontWeight.w500,
label: state.subscription.label,
label: state.subscriptionInfo.label,
buttonLabel: LocaleKeys
.settings_billingPage_plan_planButtonLabel
.tr(),
minWidth: _buttonsMinWidth,
),
if (billingPortalEnabled)
SingleSettingAction(
onPressed: () =>
afLaunchUrlString(state.billingPortal!.url),
onPressed: () {
SettingsAlertDialog(
title: LocaleKeys
.settings_billingPage_changePeriod
.tr(),
enableConfirmNotifier: enablePlanChangeNotifier,
children: [
ChangePeriod(
plan: state.subscriptionInfo.planSubscription
.subscriptionPlan,
selectedInterval: state.subscriptionInfo
.planSubscription.interval,
onSelected: (interval) {
enablePlanChangeNotifier.value = interval !=
state.subscriptionInfo.planSubscription
.interval;
selectedInterval = interval;
},
),
],
confirm: () {
if (selectedInterval !=
state.subscriptionInfo.planSubscription
.interval) {
context.read<SettingsBillingBloc>().add(
SettingsBillingEvent.updatePeriod(
plan: state
.subscriptionInfo
.planSubscription
.subscriptionPlan,
interval: selectedInterval!,
),
);
}
Navigator.of(context).pop();
},
).show(context);
},
label: LocaleKeys
.settings_billingPage_plan_billingPeriod
.tr(),
description: state
.subscriptionInfo.planSubscription.interval.label,
fontWeight: FontWeight.w500,
buttonLabel: LocaleKeys
.settings_billingPage_plan_periodButtonLabel
.tr(),
minWidth: _buttonsMinWidth,
),
],
),
@ -99,8 +173,11 @@ class SettingsBillingView extends StatelessWidget {
.tr(),
children: [
SingleSettingAction(
onPressed: () =>
afLaunchUrlString(state.billingPortal!.url),
onPressed: () => context
.read<SettingsBillingBloc>()
.add(
const SettingsBillingEvent.openCustomerPortal(),
),
label: LocaleKeys
.settings_billingPage_paymentDetails_methodLabel
.tr(),
@ -108,9 +185,48 @@ class SettingsBillingView extends StatelessWidget {
buttonLabel: LocaleKeys
.settings_billingPage_paymentDetails_methodButtonLabel
.tr(),
minWidth: _buttonsMinWidth,
),
],
),
SettingsCategory(
title: LocaleKeys.settings_billingPage_addons_title.tr(),
children: [
_AITile(
plan: SubscriptionPlanPB.AiMax,
label: LocaleKeys
.settings_billingPage_addons_aiMax_label
.tr(),
description: LocaleKeys
.settings_billingPage_addons_aiMax_description,
activeDescription: LocaleKeys
.settings_billingPage_addons_aiMax_activeDescription,
canceledDescription: LocaleKeys
.settings_billingPage_addons_aiMax_canceledDescription,
subscriptionInfo:
state.subscriptionInfo.addOns.firstWhereOrNull(
(a) => a.type == WorkspaceAddOnPBType.AddOnAiMax,
),
),
const SettingsDashedDivider(),
_AITile(
plan: SubscriptionPlanPB.AiLocal,
label: LocaleKeys
.settings_billingPage_addons_aiOnDevice_label
.tr(),
description: LocaleKeys
.settings_billingPage_addons_aiOnDevice_description,
activeDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_activeDescription,
canceledDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_canceledDescription,
subscriptionInfo:
state.subscriptionInfo.addOns.firstWhereOrNull(
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
),
),
],
),
],
);
},
@ -124,17 +240,17 @@ class SettingsBillingView extends StatelessWidget {
BuildContext context,
String workspaceId,
Int64 userId,
WorkspaceSubscriptionPB subscription,
WorkspaceSubscriptionInfoPB subscriptionInfo,
) =>
showDialog<bool?>(
context: context,
builder: (_) => BlocProvider<SettingsPlanBloc>(
create: (_) =>
SettingsPlanBloc(workspaceId: workspaceId, userId: user.id)
SettingsPlanBloc(workspaceId: workspaceId, userId: widget.user.id)
..add(const SettingsPlanEvent.started()),
child: SettingsPlanComparisonDialog(
workspaceId: workspaceId,
subscription: subscription,
subscriptionInfo: subscriptionInfo,
),
),
).then((didChangePlan) {
@ -145,3 +261,341 @@ class SettingsBillingView extends StatelessWidget {
}
});
}
class _AITile extends StatefulWidget {
const _AITile({
required this.label,
required this.description,
required this.canceledDescription,
required this.activeDescription,
required this.plan,
this.subscriptionInfo,
});
final String label;
final String description;
final String canceledDescription;
final String activeDescription;
final SubscriptionPlanPB plan;
final WorkspaceAddOnPB? subscriptionInfo;
@override
State<_AITile> createState() => _AITileState();
}
class _AITileState extends State<_AITile> {
RecurringIntervalPB? selectedInterval;
final enableConfirmNotifier = ValueNotifier<bool>(false);
@override
Widget build(BuildContext context) {
final isCanceled = widget.subscriptionInfo?.addOnSubscription.status ==
WorkspaceSubscriptionStatusPB.Canceled;
final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;
return Column(
children: [
SingleSettingAction(
label: widget.label,
description: widget.subscriptionInfo != null && isCanceled
? widget.canceledDescription.tr(
args: [
dateFormat.formatDate(
widget.subscriptionInfo!.addOnSubscription.endDate
.toDateTime(),
false,
),
],
)
: widget.subscriptionInfo != null
? widget.activeDescription.tr(
args: [
dateFormat.formatDate(
widget.subscriptionInfo!.addOnSubscription.endDate
.toDateTime(),
false,
),
],
)
: widget.description.tr(),
buttonLabel: widget.subscriptionInfo != null
? isCanceled
? LocaleKeys.settings_billingPage_addons_renewLabel.tr()
: LocaleKeys.settings_billingPage_addons_removeLabel.tr()
: LocaleKeys.settings_billingPage_addons_addLabel.tr(),
fontWeight: FontWeight.w500,
minWidth: _buttonsMinWidth,
onPressed: () {
if (widget.subscriptionInfo != null && isCanceled) {
// Show customer portal to renew
context
.read<SettingsBillingBloc>()
.add(const SettingsBillingEvent.openCustomerPortal());
} else if (widget.subscriptionInfo != null) {
showConfirmDialog(
context: context,
style: ConfirmPopupStyle.cancelAndOk,
title: LocaleKeys.settings_billingPage_addons_removeDialog_title
.tr(args: [widget.plan.label]).tr(),
description: LocaleKeys
.settings_billingPage_addons_removeDialog_description
.tr(namedArgs: {"plan": widget.plan.label.tr()}),
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () {
context.read<SettingsBillingBloc>().add(
SettingsBillingEvent.cancelSubscription(widget.plan),
);
},
);
} else {
// Add the addon
context
.read<SettingsBillingBloc>()
.add(SettingsBillingEvent.addSubscription(widget.plan));
}
},
),
if (widget.subscriptionInfo != null) ...[
const VSpace(10),
SingleSettingAction(
label: LocaleKeys.settings_billingPage_planPeriod.tr(
args: [
widget
.subscriptionInfo!.addOnSubscription.subscriptionPlan.label,
],
),
description:
widget.subscriptionInfo!.addOnSubscription.interval.label,
buttonLabel:
LocaleKeys.settings_billingPage_plan_periodButtonLabel.tr(),
minWidth: _buttonsMinWidth,
onPressed: () {
enableConfirmNotifier.value = false;
SettingsAlertDialog(
title: LocaleKeys.settings_billingPage_changePeriod.tr(),
enableConfirmNotifier: enableConfirmNotifier,
children: [
ChangePeriod(
plan: widget
.subscriptionInfo!.addOnSubscription.subscriptionPlan,
selectedInterval:
widget.subscriptionInfo!.addOnSubscription.interval,
onSelected: (interval) {
enableConfirmNotifier.value = interval !=
widget.subscriptionInfo!.addOnSubscription.interval;
selectedInterval = interval;
},
),
],
confirm: () {
if (selectedInterval !=
widget.subscriptionInfo!.addOnSubscription.interval) {
context.read<SettingsBillingBloc>().add(
SettingsBillingEvent.updatePeriod(
plan: widget.subscriptionInfo!.addOnSubscription
.subscriptionPlan,
interval: selectedInterval!,
),
);
}
Navigator.of(context).pop();
},
).show(context);
},
),
],
],
);
}
}
class ChangePeriod extends StatefulWidget {
const ChangePeriod({
super.key,
required this.plan,
required this.selectedInterval,
required this.onSelected,
});
final SubscriptionPlanPB plan;
final RecurringIntervalPB selectedInterval;
final Function(RecurringIntervalPB interval) onSelected;
@override
State<ChangePeriod> createState() => _ChangePeriodState();
}
class _ChangePeriodState extends State<ChangePeriod> {
RecurringIntervalPB? _selectedInterval;
@override
void initState() {
super.initState();
_selectedInterval = widget.selectedInterval;
}
@override
void didChangeDependencies() {
_selectedInterval = widget.selectedInterval;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_PeriodSelector(
price: widget.plan.priceMonthBilling,
interval: RecurringIntervalPB.Month,
isSelected: _selectedInterval == RecurringIntervalPB.Month,
isCurrent: widget.selectedInterval == RecurringIntervalPB.Month,
onSelected: () {
widget.onSelected(RecurringIntervalPB.Month);
setState(
() => _selectedInterval = RecurringIntervalPB.Month,
);
},
),
const VSpace(16),
_PeriodSelector(
price: widget.plan.priceAnnualBilling,
interval: RecurringIntervalPB.Year,
isSelected: _selectedInterval == RecurringIntervalPB.Year,
isCurrent: widget.selectedInterval == RecurringIntervalPB.Year,
onSelected: () {
widget.onSelected(RecurringIntervalPB.Year);
setState(
() => _selectedInterval = RecurringIntervalPB.Year,
);
},
),
],
);
}
}
class _PeriodSelector extends StatelessWidget {
const _PeriodSelector({
required this.price,
required this.interval,
required this.onSelected,
required this.isSelected,
required this.isCurrent,
});
final String price;
final RecurringIntervalPB interval;
final VoidCallback onSelected;
final bool isSelected;
final bool isCurrent;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: isCurrent && !isSelected ? 0.7 : 1,
child: GestureDetector(
onTap: isCurrent ? null : onSelected,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FlowyText(
interval.label,
fontSize: 16,
fontWeight: FontWeight.w500,
),
if (isCurrent) ...[
const HSpace(8),
DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 1,
),
child: FlowyText(
LocaleKeys
.settings_billingPage_currentPeriodBadge
.tr(),
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
],
],
),
const VSpace(8),
FlowyText(
price,
fontSize: 14,
fontWeight: FontWeight.w500,
),
const VSpace(4),
FlowyText(
interval.priceInfo,
fontWeight: FontWeight.w400,
fontSize: 12,
),
],
),
const Spacer(),
if (!isCurrent && !isSelected || isSelected) ...[
DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 1.5,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).dividerColor,
),
),
child: SizedBox(
height: 22,
width: 22,
child: Center(
child: SizedBox(
width: 10,
height: 10,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
),
),
),
),
),
],
],
),
),
),
),
);
}
}

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -12,7 +15,6 @@ import 'package:appflowy/workspace/application/settings/settings_location_cubit.
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
@ -27,8 +29,6 @@ import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -63,15 +63,15 @@ class SettingsManageDataView extends StatelessWidget {
size: Size.square(20),
),
label: LocaleKeys.settings_common_reset.tr(),
onPressed: () => SettingsAlertDialog(
onPressed: () => showConfirmDialog(
context: context,
title: LocaleKeys
.settings_manageDataPage_dataStorage_resetDialog_title
.tr(),
subtitle: LocaleKeys
description: LocaleKeys
.settings_manageDataPage_dataStorage_resetDialog_description
.tr(),
implyLeading: true,
confirm: () async {
onConfirm: () async {
final directory =
await appFlowyApplicationDataDirectory();
final path = directory.path;
@ -85,10 +85,8 @@ class SettingsManageDataView extends StatelessWidget {
.read<SettingsLocationCubit>()
.resetDataStoragePathToApplicationDefault();
await runAppFlowy(isAnon: true);
if (context.mounted) Navigator.of(context).pop();
},
).show(context),
),
),
],
children: state

View File

@ -1,27 +1,30 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../generated/locale_keys.g.dart';
import '../../../../plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
class SettingsPlanComparisonDialog extends StatefulWidget {
const SettingsPlanComparisonDialog({
super.key,
required this.workspaceId,
required this.subscription,
required this.subscriptionInfo,
});
final String workspaceId;
final WorkspaceSubscriptionPB subscription;
final WorkspaceSubscriptionInfoPB subscriptionInfo;
@override
State<SettingsPlanComparisonDialog> createState() =>
@ -33,7 +36,9 @@ class _SettingsPlanComparisonDialogState
final horizontalController = ScrollController();
final verticalController = ScrollController();
late WorkspaceSubscriptionPB currentSubscription = widget.subscription;
late WorkspaceSubscriptionInfoPB currentInfo = widget.subscriptionInfo;
Loading? loadingIndicator;
@override
void dispose() {
@ -54,32 +59,27 @@ class _SettingsPlanComparisonDialogState
return;
}
if (readyState.showSuccessDialog) {
SettingsAlertDialog(
icon: Center(
child: SizedBox(
height: 90,
width: 90,
child: FlowySvg(
FlowySvgs.check_circle_s,
color: AFThemeExtension.of(context).success,
),
),
),
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
.tr(args: [readyState.subscription.label]),
subtitle: LocaleKeys
.settings_comparePlanDialog_paymentSuccess_description
.tr(args: [readyState.subscription.label]),
hideCancelButton: true,
confirm: Navigator.of(context).pop,
confirmLabel: LocaleKeys.button_close.tr(),
).show(context);
if (readyState.downgradeProcessing) {
loadingIndicator = Loading(context)..start();
} else {
loadingIndicator?.stop();
loadingIndicator = null;
}
setState(() {
currentSubscription = readyState.subscription;
});
if (readyState.successfulPlanUpgrade != null) {
showConfirmDialog(
context: context,
title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title
.tr(args: [readyState.successfulPlanUpgrade!.label]),
description: LocaleKeys
.settings_comparePlanDialog_paymentSuccess_description
.tr(args: [readyState.successfulPlanUpgrade!.label]),
confirmLabel: LocaleKeys.button_close.tr(),
onConfirm: () {},
);
}
setState(() => currentInfo = readyState.subscriptionInfo);
},
builder: (context, state) => FlowyDialog(
constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),
@ -99,8 +99,7 @@ class _SettingsPlanComparisonDialogState
const Spacer(),
GestureDetector(
onTap: () => Navigator.of(context).pop(
currentSubscription.subscriptionPlan !=
widget.subscription.subscriptionPlan,
currentInfo.plan != widget.subscriptionInfo.plan,
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
@ -154,7 +153,7 @@ class _SettingsPlanComparisonDialogState
: const Color(0xFFE8E0FF),
),
),
const SizedBox(height: 64),
const SizedBox(height: 96),
const SizedBox(height: 56),
..._planLabels.map(
(e) => _ComparisonCell(
@ -172,56 +171,55 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_freePlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_freePlan_price
.tr(args: ['\$0']),
.tr(
args: [
SubscriptionPlanPB.Free.priceMonthBilling,
],
),
priceInfo: LocaleKeys
.settings_comparePlanDialog_freePlan_priceInfo
.tr(),
cells: _freeLabels,
isCurrent: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None,
isCurrent:
currentInfo.plan == WorkspacePlanPB.FreePlan,
canDowngrade:
currentSubscription.subscriptionPlan !=
SubscriptionPlanPB.None,
currentCanceled: currentSubscription.hasCanceled ||
currentInfo.plan != WorkspacePlanPB.FreePlan,
currentCanceled: currentInfo.isCanceled ||
(context
.watch<SettingsPlanBloc>()
.state
.mapOrNull(
loading: (_) => true,
ready: (state) =>
state.downgradeProcessing,
ready: (s) => s.downgradeProcessing,
) ??
false),
onSelected: () async {
if (currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None ||
currentSubscription.hasCanceled) {
if (currentInfo.plan ==
WorkspacePlanPB.FreePlan ||
currentInfo.isCanceled) {
return;
}
await SettingsAlertDialog(
await showConfirmDialog(
context: context,
title: LocaleKeys
.settings_comparePlanDialog_downgradeDialog_title
.tr(args: [currentSubscription.label]),
subtitle: LocaleKeys
.tr(args: [currentInfo.label]),
description: LocaleKeys
.settings_comparePlanDialog_downgradeDialog_description
.tr(),
isDangerous: true,
confirm: () {
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent
.cancelSubscription(),
);
Navigator.of(context).pop();
},
confirmLabel: LocaleKeys
.settings_comparePlanDialog_downgradeDialog_downgradeLabel
.tr(),
).show(context);
style: ConfirmPopupStyle.cancelAndOk,
onConfirm: () =>
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent
.cancelSubscription(),
),
);
},
),
_PlanTable(
@ -231,19 +229,22 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_proPlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_proPlan_price
.tr(args: ['\$10 ']),
.tr(
args: [SubscriptionPlanPB.Pro.priceAnnualBilling],
),
priceInfo: LocaleKeys
.settings_comparePlanDialog_proPlan_priceInfo
.tr(),
.tr(
args: [SubscriptionPlanPB.Pro.priceMonthBilling],
),
cells: _proLabels,
isCurrent: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.Pro,
canUpgrade: currentSubscription.subscriptionPlan ==
SubscriptionPlanPB.None,
currentCanceled: currentSubscription.hasCanceled,
isCurrent:
currentInfo.plan == WorkspacePlanPB.ProPlan,
canUpgrade:
currentInfo.plan == WorkspacePlanPB.FreePlan,
currentCanceled: currentInfo.isCanceled,
onSelected: () =>
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent.addSubscription(
@ -335,7 +336,7 @@ class _PlanTable extends StatelessWidget {
title: price,
description: priceInfo,
isPrimary: !highlightPlan,
height: 64,
height: 96,
),
if (canUpgrade || canDowngrade) ...[
Opacity(
@ -589,21 +590,28 @@ class _Heading extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: 175,
width: 185,
height: height,
child: Padding(
padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(
title,
fontSize: 24,
color: isPrimary
? AFThemeExtension.of(context).strongText
: Theme.of(context).isLightMode
? const Color(0xFF5C3699)
: const Color(0xFFC49BEC),
Row(
children: [
Expanded(
child: FlowyText.semibold(
title,
fontSize: 24,
overflow: TextOverflow.ellipsis,
color: isPrimary
? AFThemeExtension.of(context).strongText
: Theme.of(context).isLightMode
? const Color(0xFF5C3699)
: const Color(0xFFC49BEC),
),
),
],
),
if (description != null && description!.isNotEmpty) ...[
const VSpace(4),
@ -637,24 +645,20 @@ final _planLabels = [
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(),
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipThree.tr(),
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.tr(),
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipFour.tr(),
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFive.tr(),
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSix.tr(),
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(),
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSeven.tr(),
),
_PlanItem(
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemEight.tr(),
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipEight.tr(),
tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSeven.tr(),
),
];
@ -677,20 +681,17 @@ final List<_CellItem> _freeLabels = [
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_freeLabels_itemEight.tr(),
),
];
@ -706,19 +707,17 @@ final List<_CellItem> _proLabels = [
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(),
icon: FlowySvgs.check_m,
),
_CellItem(
LocaleKeys.settings_comparePlanDialog_proLabels_itemEight.tr(),
),
];

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/int64_extension.dart';
@ -14,13 +16,15 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsPlanView extends StatelessWidget {
import '../../../../plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
class SettingsPlanView extends StatefulWidget {
const SettingsPlanView({
super.key,
required this.workspaceId,
@ -30,14 +34,32 @@ class SettingsPlanView extends StatelessWidget {
final String workspaceId;
final UserProfilePB user;
@override
State<SettingsPlanView> createState() => _SettingsPlanViewState();
}
class _SettingsPlanViewState extends State<SettingsPlanView> {
Loading? loadingIndicator;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsPlanBloc>(
create: (context) => SettingsPlanBloc(
workspaceId: workspaceId,
userId: user.id,
workspaceId: widget.workspaceId,
userId: widget.user.id,
)..add(const SettingsPlanEvent.started()),
child: BlocBuilder<SettingsPlanBloc, SettingsPlanState>(
child: BlocConsumer<SettingsPlanBloc, SettingsPlanState>(
listenWhen: (previous, current) =>
previous.mapOrNull(ready: (s) => s.downgradeProcessing) !=
current.mapOrNull(ready: (s) => s.downgradeProcessing),
listener: (context, state) {
if (state.mapOrNull(ready: (s) => s.downgradeProcessing) == true) {
loadingIndicator = Loading(context)..start();
} else {
loadingIndicator?.stop();
loadingIndicator = null;
}
},
builder: (context, state) {
return state.map(
initial: (_) => const SizedBox.shrink(),
@ -67,10 +89,87 @@ class SettingsPlanView extends StatelessWidget {
children: [
_PlanUsageSummary(
usage: state.workspaceUsage,
subscription: state.subscription,
subscriptionInfo: state.subscriptionInfo,
),
const VSpace(16),
_CurrentPlanBox(subscription: state.subscription),
_CurrentPlanBox(subscriptionInfo: state.subscriptionInfo),
const VSpace(16),
FlowyText(
LocaleKeys.settings_planPage_planUsage_addons_title.tr(),
fontSize: 18,
color: AFThemeExtension.of(context).strongText,
fontWeight: FontWeight.w600,
),
const VSpace(8),
Row(
children: [
Flexible(
child: _AddOnBox(
title: LocaleKeys
.settings_planPage_planUsage_addons_aiMax_title
.tr(),
description: LocaleKeys
.settings_planPage_planUsage_addons_aiMax_description
.tr(),
price: LocaleKeys
.settings_planPage_planUsage_addons_aiMax_price
.tr(
args: [SubscriptionPlanPB.AiMax.priceAnnualBilling],
),
priceInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiMax_priceInfo
.tr(),
billingInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiMax_billingInfo
.tr(
args: [SubscriptionPlanPB.AiMax.priceMonthBilling],
),
buttonText: state.subscriptionInfo.hasAIMax
? LocaleKeys
.settings_planPage_planUsage_addons_activeLabel
.tr()
: LocaleKeys
.settings_planPage_planUsage_addons_addLabel
.tr(),
isActive: state.subscriptionInfo.hasAIMax,
plan: SubscriptionPlanPB.AiMax,
),
),
const HSpace(8),
Flexible(
child: _AddOnBox(
title: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_title
.tr(),
description: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_description
.tr(),
price: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_price
.tr(
args: [SubscriptionPlanPB.AiLocal.priceAnnualBilling],
),
priceInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
.tr(),
billingInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
.tr(
args: [SubscriptionPlanPB.AiLocal.priceMonthBilling],
),
buttonText: state.subscriptionInfo.hasAIOnDevice
? LocaleKeys
.settings_planPage_planUsage_addons_activeLabel
.tr()
: LocaleKeys
.settings_planPage_planUsage_addons_addLabel
.tr(),
isActive: state.subscriptionInfo.hasAIOnDevice,
plan: SubscriptionPlanPB.AiLocal,
),
),
],
),
],
),
);
@ -81,9 +180,9 @@ class SettingsPlanView extends StatelessWidget {
}
class _CurrentPlanBox extends StatefulWidget {
const _CurrentPlanBox({required this.subscription});
const _CurrentPlanBox({required this.subscriptionInfo});
final WorkspaceSubscriptionPB subscription;
final WorkspaceSubscriptionInfoPB subscriptionInfo;
@override
State<_CurrentPlanBox> createState() => _CurrentPlanBoxState();
@ -115,68 +214,67 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
border: Border.all(color: const Color(0xFFBDBDBD)),
borderRadius: BorderRadius.circular(16),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
child: Column(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(4),
FlowyText.semibold(
widget.subscription.label,
fontSize: 24,
color: AFThemeExtension.of(context).strongText,
),
const VSpace(8),
FlowyText.regular(
widget.subscription.info,
fontSize: 16,
color: AFThemeExtension.of(context).strongText,
maxLines: 3,
),
const VSpace(16),
FlowyGradientButton(
label: LocaleKeys
.settings_planPage_planUsage_currentPlan_upgrade
.tr(),
onPressed: () => _openPricingDialog(
context,
context.read<SettingsPlanBloc>().workspaceId,
widget.subscription,
),
),
if (widget.subscription.hasCanceled) ...[
const VSpace(12),
FlowyText(
LocaleKeys
.settings_planPage_planUsage_currentPlan_canceledInfo
.tr(
args: [_canceledDate(context)],
Row(
children: [
Expanded(
flex: 6,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(4),
FlowyText.semibold(
widget.subscriptionInfo.label,
fontSize: 24,
color: AFThemeExtension.of(context).strongText,
),
maxLines: 5,
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
],
],
),
),
const HSpace(16),
Expanded(
child: SeparatedColumn(
separatorBuilder: () => const VSpace(4),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._getPros(widget.subscription.subscriptionPlan).map(
(s) => _ProConItem(label: s),
const VSpace(8),
FlowyText.regular(
widget.subscriptionInfo.info,
fontSize: 16,
color: AFThemeExtension.of(context).strongText,
maxLines: 3,
),
],
),
..._getCons(widget.subscription.subscriptionPlan).map(
(s) => _ProConItem(label: s, isPro: false),
),
Flexible(
flex: 5,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 220),
child: FlowyGradientButton(
label: LocaleKeys
.settings_planPage_planUsage_currentPlan_upgrade
.tr(),
onPressed: () => _openPricingDialog(
context,
context.read<SettingsPlanBloc>().workspaceId,
widget.subscriptionInfo,
),
),
),
],
),
],
),
),
],
),
if (widget.subscriptionInfo.isCanceled) ...[
const VSpace(12),
FlowyText(
LocaleKeys
.settings_planPage_planUsage_currentPlan_canceledInfo
.tr(
args: [_canceledDate(context)],
),
maxLines: 5,
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
],
],
),
),
@ -184,14 +282,21 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
top: 0,
left: 0,
child: Container(
height: 32,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: const BoxDecoration(color: Color(0xFF4F3F5F)),
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: const BoxDecoration(
color: Color(0xFF4F3F5F),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
bottomRight: Radius.circular(4),
),
),
child: Center(
child: FlowyText.semibold(
LocaleKeys.settings_planPage_planUsage_currentPlan_bannerLabel
.tr(),
fontSize: 16,
fontSize: 14,
color: Colors.white,
),
),
@ -204,16 +309,15 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
String _canceledDate(BuildContext context) {
final appearance = context.read<AppearanceSettingsCubit>().state;
return appearance.dateFormat.formatDate(
widget.subscription.canceledAt.toDateTime(),
true,
appearance.timeFormat,
widget.subscriptionInfo.planSubscription.endDate.toDateTime(),
false,
);
}
void _openPricingDialog(
BuildContext context,
String workspaceId,
WorkspaceSubscriptionPB subscription,
WorkspaceSubscriptionInfoPB subscriptionInfo,
) =>
showDialog(
context: context,
@ -221,101 +325,20 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> {
value: planBloc,
child: SettingsPlanComparisonDialog(
workspaceId: workspaceId,
subscription: subscription,
subscriptionInfo: subscriptionInfo,
),
),
);
List<String> _getPros(SubscriptionPlanPB plan) => switch (plan) {
SubscriptionPlanPB.Pro => _proPros(),
_ => _freePros(),
};
List<String> _getCons(SubscriptionPlanPB plan) => switch (plan) {
SubscriptionPlanPB.Pro => _proCons(),
_ => _freeCons(),
};
List<String> _freePros() => [
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProOne.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProTwo.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProThree.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFour.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFive.tr(),
];
List<String> _freeCons() => [
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConOne.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConTwo.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_freeConThree.tr(),
];
List<String> _proPros() => [
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProOne
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProTwo
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProThree
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFour
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFive
.tr(),
];
List<String> _proCons() => [
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConOne
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConTwo
.tr(),
LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConThree
.tr(),
];
}
class _ProConItem extends StatelessWidget {
const _ProConItem({
required this.label,
this.isPro = true,
});
final String label;
final bool isPro;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 18,
child: FlowySvg(
isPro ? FlowySvgs.check_m : FlowySvgs.close_error_s,
size: const Size.square(18),
color: isPro
? AFThemeExtension.of(context).strongText
: const Color(0xFF900000),
),
),
const HSpace(4),
Flexible(
child: FlowyText.regular(
label,
fontSize: 12,
color: AFThemeExtension.of(context).strongText,
maxLines: 3,
),
),
],
);
}
}
class _PlanUsageSummary extends StatelessWidget {
const _PlanUsageSummary({required this.usage, required this.subscription});
const _PlanUsageSummary({
required this.usage,
required this.subscriptionInfo,
});
final WorkspaceUsagePB usage;
final WorkspaceSubscriptionPB subscription;
final WorkspaceSubscriptionInfoPB subscriptionInfo;
@override
Widget build(BuildContext context) {
@ -331,61 +354,101 @@ class _PlanUsageSummary extends StatelessWidget {
),
const VSpace(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _UsageBox(
title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(),
replacementText: subscription.subscriptionPlan ==
SubscriptionPlanPB.Pro
? LocaleKeys.settings_planPage_planUsage_storageUnlimited
.tr()
: null,
unlimitedLabel: LocaleKeys
.settings_planPage_planUsage_unlimitedStorageLabel
.tr(),
unlimited: usage.storageBytesUnlimited,
label: LocaleKeys.settings_planPage_planUsage_storageUsage.tr(
args: [
usage.currentBlobInGb,
usage.totalBlobInGb,
],
),
value: usage.totalBlobBytes.toInt() /
usage.totalBlobBytesLimit.toInt(),
value: usage.storageBytes.toInt() /
usage.storageBytesLimit.toInt(),
),
),
Expanded(
child: _UsageBox(
title: LocaleKeys.settings_planPage_planUsage_collaboratorsLabel
.tr(),
label: LocaleKeys.settings_planPage_planUsage_collaboratorsUsage
.tr(
title:
LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(),
label:
LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr(
args: [
usage.memberCount.toString(),
usage.memberCountLimit.toString(),
usage.aiResponsesCount.toString(),
usage.aiResponsesCountLimit.toString(),
],
),
value:
usage.memberCount.toInt() / usage.memberCountLimit.toInt(),
unlimitedLabel: LocaleKeys
.settings_planPage_planUsage_unlimitedAILabel
.tr(),
unlimited: usage.aiResponsesUnlimited,
value: usage.aiResponsesCount.toInt() /
usage.aiResponsesCountLimit.toInt(),
),
),
],
),
const VSpace(16),
Column(
SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const VSpace(4),
children: [
_ToggleMore(
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
label:
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
subscription: subscription,
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
),
const VSpace(8),
_ToggleMore(
value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro,
label:
LocaleKeys.settings_planPage_planUsage_guestCollabToggle.tr(),
subscription: subscription,
badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
),
if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[
_ToggleMore(
value: false,
label:
LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),
badgeLabel:
LocaleKeys.settings_planPage_planUsage_proBadge.tr(),
onTap: () async {
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent.addSubscription(
SubscriptionPlanPB.Pro,
),
);
await Future.delayed(const Duration(seconds: 2), () {});
},
),
],
if (!subscriptionInfo.hasAIMax && !usage.aiResponsesUnlimited) ...[
_ToggleMore(
value: false,
label: LocaleKeys.settings_planPage_planUsage_aiMaxToggle.tr(),
badgeLabel:
LocaleKeys.settings_planPage_planUsage_aiMaxBadge.tr(),
onTap: () async {
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent.addSubscription(
SubscriptionPlanPB.AiMax,
),
);
await Future.delayed(const Duration(seconds: 2), () {});
},
),
],
if (!subscriptionInfo.hasAIOnDevice) ...[
_ToggleMore(
value: false,
label: LocaleKeys.settings_planPage_planUsage_aiOnDeviceToggle
.tr(),
badgeLabel:
LocaleKeys.settings_planPage_planUsage_aiOnDeviceBadge.tr(),
onTap: () async {
context.read<SettingsPlanBloc>().add(
const SettingsPlanEvent.addSubscription(
SubscriptionPlanPB.AiLocal,
),
);
await Future.delayed(const Duration(seconds: 2), () {});
},
),
],
],
),
],
@ -398,15 +461,18 @@ class _UsageBox extends StatelessWidget {
required this.title,
required this.label,
required this.value,
this.replacementText,
required this.unlimitedLabel,
this.unlimited = false,
});
final String title;
final String label;
final double value;
/// Replaces the progress indicator if not null
final String? replacementText;
final String unlimitedLabel;
// Replaces the progress bar with an unlimited badge
final bool unlimited;
@override
Widget build(BuildContext context) {
@ -418,19 +484,27 @@ class _UsageBox extends StatelessWidget {
fontSize: 11,
color: AFThemeExtension.of(context).secondaryTextColor,
),
if (replacementText != null) ...[
Row(
children: [
Flexible(
child: FlowyText.medium(
replacementText!,
fontSize: 11,
color: AFThemeExtension.of(context).secondaryTextColor,
if (unlimited) ...[
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.check_circle_outlined_s,
color: Color(0xFF9C00FB),
),
),
],
const HSpace(4),
FlowyText(
unlimitedLabel,
fontWeight: FontWeight.w500,
fontSize: 11,
),
],
),
),
] else ...[
const VSpace(4),
_PlanProgressIndicator(label: label, progress: value),
],
],
@ -442,14 +516,14 @@ class _ToggleMore extends StatefulWidget {
const _ToggleMore({
required this.value,
required this.label,
required this.subscription,
this.badgeLabel,
this.onTap,
});
final bool value;
final String label;
final WorkspaceSubscriptionPB subscription;
final String? badgeLabel;
final Future<void> Function()? onTap;
@override
State<_ToggleMore> createState() => _ToggleMoreState();
@ -471,29 +545,17 @@ class _ToggleMoreState extends State<_ToggleMore> {
Toggle(
value: toggleValue,
padding: EdgeInsets.zero,
onChanged: (_) {
setState(() => toggleValue = !toggleValue);
onChanged: (_) async {
if (widget.onTap == null || toggleValue) {
return;
}
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted) {
showDialog(
context: context,
builder: (_) => BlocProvider<SettingsPlanBloc>.value(
value: context.read<SettingsPlanBloc>(),
child: SettingsPlanComparisonDialog(
workspaceId: context.read<SettingsPlanBloc>().workspaceId,
subscription: widget.subscription,
),
),
).then((_) {
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => toggleValue = !toggleValue);
}
});
});
}
});
setState(() => toggleValue = !toggleValue);
await widget.onTap!();
if (mounted) {
setState(() => toggleValue = !toggleValue);
}
},
),
const HSpace(10),
@ -553,7 +615,9 @@ class _PlanProgressIndicator extends StatelessWidget {
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
color: progress >= 1
? theme.colorScheme.error
: theme.colorScheme.primary,
),
),
),
@ -574,6 +638,135 @@ class _PlanProgressIndicator extends StatelessWidget {
}
}
class _AddOnBox extends StatelessWidget {
const _AddOnBox({
required this.title,
required this.description,
required this.price,
required this.priceInfo,
required this.billingInfo,
required this.buttonText,
required this.isActive,
required this.plan,
});
final String title;
final String description;
final String price;
final String priceInfo;
final String billingInfo;
final String buttonText;
final bool isActive;
final SubscriptionPlanPB plan;
@override
Widget build(BuildContext context) {
final isLM = Theme.of(context).isLightMode;
return Container(
height: 220,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
border: Border.all(
color: isActive ? const Color(0xFFBDBDBD) : const Color(0xFF9C00FB),
),
color: const Color(0xFFF7F8FC).withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(
title,
fontSize: 14,
color: AFThemeExtension.of(context).strongText,
),
const VSpace(10),
FlowyText.regular(
description,
fontSize: 12,
color: AFThemeExtension.of(context).secondaryTextColor,
maxLines: 4,
),
const VSpace(10),
FlowyText(
price,
fontSize: 24,
color: AFThemeExtension.of(context).strongText,
),
FlowyText(
priceInfo,
fontSize: 12,
color: AFThemeExtension.of(context).strongText,
),
const VSpace(12),
Row(
children: [
Expanded(
child: FlowyText(
billingInfo,
color: AFThemeExtension.of(context).secondaryTextColor,
fontSize: 11,
maxLines: 2,
),
),
],
),
const Spacer(),
Row(
children: [
Expanded(
child: FlowyTextButton(
buttonText,
heading: isActive
? const FlowySvg(
FlowySvgs.check_circle_outlined_s,
color: Color(0xFF9C00FB),
)
: null,
mainAxisAlignment: MainAxisAlignment.center,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
fillColor: isActive
? const Color(0xFFE8E2EE)
: isLM
? Colors.transparent
: const Color(0xFF5C3699),
constraints: const BoxConstraints(minWidth: 115),
radius: Corners.s16Border,
hoverColor: isActive
? const Color(0xFFE8E2EE)
: isLM
? const Color(0xFF5C3699)
: const Color(0xFF4d3472),
fontColor:
isLM || isActive ? const Color(0xFF5C3699) : Colors.white,
fontHoverColor:
isActive ? const Color(0xFF5C3699) : Colors.white,
borderColor: isActive
? const Color(0xFFE8E2EE)
: isLM
? const Color(0xFF5C3699)
: const Color(0xFF4d3472),
fontSize: 12,
onPressed: isActive
? null
: () => context
.read<SettingsPlanBloc>()
.add(SettingsPlanEvent.addSubscription(plan)),
),
),
],
),
],
),
);
}
}
/// Uncomment if we need it in the future
// class _DealBox extends StatelessWidget {
// const _DealBox();

View File

@ -22,7 +22,6 @@ import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu
import 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
@ -183,7 +182,8 @@ class SettingsWorkspaceView extends StatelessWidget {
.tr(),
fontSize: 16,
fontWeight: FontWeight.w600,
onPressed: () => SettingsAlertDialog(
onPressed: () => showConfirmDialog(
context: context,
title: workspaceMember?.role.isOwner ?? false
? LocaleKeys
.settings_workspacePage_deleteWorkspacePrompt_title
@ -191,24 +191,21 @@ class SettingsWorkspaceView extends StatelessWidget {
: LocaleKeys
.settings_workspacePage_leaveWorkspacePrompt_title
.tr(),
subtitle: workspaceMember?.role.isOwner ?? false
description: workspaceMember?.role.isOwner ?? false
? LocaleKeys
.settings_workspacePage_deleteWorkspacePrompt_content
.tr()
: LocaleKeys
.settings_workspacePage_leaveWorkspacePrompt_content
.tr(),
isDangerous: true,
confirm: () {
context.read<WorkspaceSettingsBloc>().add(
workspaceMember?.role.isOwner ?? false
? const WorkspaceSettingsEvent.deleteWorkspace()
: const WorkspaceSettingsEvent.leaveWorkspace(),
);
Navigator.of(context).pop();
},
).show(context),
isDangerous: true,
style: ConfirmPopupStyle.cancelAndOk,
onConfirm: () => context.read<WorkspaceSettingsBloc>().add(
workspaceMember?.role.isOwner ?? false
? const WorkspaceSettingsEvent.deleteWorkspace()
: const WorkspaceSettingsEvent.leaveWorkspace(),
),
),
buttonType: SingleSettingsButtonType.danger,
buttonLabel: workspaceMember?.role.isOwner ?? false
? LocaleKeys
.settings_workspacePage_manageWorkspace_deleteWorkspace

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
@ -26,18 +25,22 @@ class SettingsDialog extends StatelessWidget {
required this.dismissDialog,
required this.didLogout,
required this.restartApp,
this.initPage,
}) : super(key: ValueKey(user.id));
final VoidCallback dismissDialog;
final VoidCallback didLogout;
final VoidCallback restartApp;
final UserProfilePB user;
final SettingsPage? initPage;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsDialogBloc>(
create: (context) => getIt<SettingsDialogBloc>(param1: user)
..add(const SettingsDialogEvent.initial()),
create: (context) => SettingsDialogBloc(
user,
initPage: initPage,
)..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
builder: (context, state) => FlowyDialog(
width: MediaQuery.of(context).size.width * 0.7,
@ -120,7 +123,10 @@ class SettingsDialog extends StatelessWidget {
return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud();
}
case SettingsPage.member:
return WorkspaceMembersPage(userProfile: user);
return WorkspaceMembersPage(
userProfile: user,
workspaceId: workspaceId,
);
case SettingsPage.plan:
return SettingsPlanView(workspaceId: workspaceId, user: user);
case SettingsPage.billing:

View File

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

View File

@ -21,6 +21,7 @@ class SettingsAlertDialog extends StatefulWidget {
this.hideCancelButton = false,
this.isDangerous = false,
this.implyLeading = false,
this.enableConfirmNotifier,
});
final Widget? icon;
@ -32,6 +33,7 @@ class SettingsAlertDialog extends StatefulWidget {
final String? confirmLabel;
final bool hideCancelButton;
final bool isDangerous;
final ValueNotifier<bool>? enableConfirmNotifier;
/// If true, a back button will show in the top left corner
final bool implyLeading;
@ -41,6 +43,37 @@ class SettingsAlertDialog extends StatefulWidget {
}
class _SettingsAlertDialogState extends State<SettingsAlertDialog> {
bool enableConfirm = true;
@override
void initState() {
super.initState();
if (widget.enableConfirmNotifier != null) {
widget.enableConfirmNotifier!.addListener(_updateEnableConfirm);
enableConfirm = widget.enableConfirmNotifier!.value;
}
}
void _updateEnableConfirm() {
setState(() => enableConfirm = widget.enableConfirmNotifier!.value);
}
@override
void dispose() {
if (widget.enableConfirmNotifier != null) {
widget.enableConfirmNotifier!.removeListener(_updateEnableConfirm);
}
super.dispose();
}
@override
void didUpdateWidget(covariant SettingsAlertDialog oldWidget) {
oldWidget.enableConfirmNotifier?.removeListener(_updateEnableConfirm);
widget.enableConfirmNotifier?.addListener(_updateEnableConfirm);
enableConfirm = widget.enableConfirmNotifier?.value ?? true;
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return StyledDialog(
@ -136,6 +169,7 @@ class _SettingsAlertDialogState extends State<SettingsAlertDialog> {
cancel: widget.cancel,
confirm: widget.confirm,
isDangerous: widget.isDangerous,
enableConfirm: enableConfirm,
),
],
),
@ -150,6 +184,7 @@ class _Actions extends StatelessWidget {
this.cancel,
this.confirm,
this.isDangerous = false,
this.enableConfirm = true,
});
final bool hideCancelButton;
@ -157,6 +192,7 @@ class _Actions extends StatelessWidget {
final VoidCallback? cancel;
final VoidCallback? confirm;
final bool isDangerous;
final bool enableConfirm;
@override
Widget build(BuildContext context) {
@ -197,14 +233,18 @@ class _Actions extends StatelessWidget {
),
radius: Corners.s12Border,
fontColor: isDangerous ? Colors.white : null,
fontHoverColor: Colors.white,
fillColor: isDangerous
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
hoverColor: isDangerous
? Theme.of(context).colorScheme.error
: const Color(0xFF005483),
onPressed: confirm,
fontHoverColor: !enableConfirm ? null : Colors.white,
fillColor: !enableConfirm
? Theme.of(context).dividerColor
: isDangerous
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
hoverColor: !enableConfirm
? Theme.of(context).dividerColor
: isDangerous
? Theme.of(context).colorScheme.error
: const Color(0xFF005483),
onPressed: enableConfirm ? confirm : null,
),
),
],

View File

@ -6,6 +6,16 @@ import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
enum SingleSettingsButtonType {
primary,
danger,
highlight;
bool get isPrimary => this == primary;
bool get isDangerous => this == danger;
bool get isHighlight => this == highlight;
}
/// This is used to describe a single setting action
///
/// This will render a simple action that takes the title,
@ -18,15 +28,18 @@ class SingleSettingAction extends StatelessWidget {
const SingleSettingAction({
super.key,
required this.label,
this.description,
this.labelMaxLines,
required this.buttonLabel,
this.onPressed,
this.isDangerous = false,
this.buttonType = SingleSettingsButtonType.primary,
this.fontSize = 14,
this.fontWeight = FontWeight.normal,
this.minWidth,
});
final String label;
final String? description;
final int? labelMaxLines;
final String buttonLabel;
@ -36,46 +49,115 @@ class SingleSettingAction extends StatelessWidget {
///
final VoidCallback? onPressed;
/// If isDangerous is true, the button will be rendered as a dangerous
/// action, with a red outline.
///
final bool isDangerous;
final SingleSettingsButtonType buttonType;
final double fontSize;
final FontWeight fontWeight;
final double? minWidth;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText(
label,
fontSize: fontSize,
fontWeight: fontWeight,
maxLines: labelMaxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).secondaryTextColor,
child: Column(
children: [
Row(
children: [
Expanded(
child: FlowyText(
label,
fontSize: fontSize,
fontWeight: fontWeight,
maxLines: labelMaxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).secondaryTextColor,
),
),
],
),
if (description != null) ...[
const VSpace(4),
Row(
children: [
Expanded(
child: FlowyText.regular(
description!,
fontSize: 11,
color: AFThemeExtension.of(context).secondaryTextColor,
maxLines: 2,
),
),
],
),
],
],
),
),
const HSpace(24),
SizedBox(
height: 32,
ConstrainedBox(
constraints: BoxConstraints(
minWidth: minWidth ?? 0.0,
maxHeight: 32,
minHeight: 32,
),
child: FlowyTextButton(
buttonLabel,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
fillColor:
isDangerous ? null : Theme.of(context).colorScheme.primary,
radius: Corners.s12Border,
hoverColor: isDangerous ? null : const Color(0xFF005483),
fontColor: isDangerous ? Theme.of(context).colorScheme.error : null,
fontHoverColor: Colors.white,
fillColor: fillColor(context),
radius: Corners.s8Border,
hoverColor: hoverColor(context),
fontColor: fontColor(context),
fontHoverColor: fontHoverColor(context),
borderColor: borderColor(context),
fontSize: 12,
isDangerous: isDangerous,
isDangerous: buttonType.isDangerous,
onPressed: onPressed,
),
),
],
);
}
Color? fillColor(BuildContext context) {
if (buttonType.isPrimary) {
return Theme.of(context).colorScheme.primary;
}
return Colors.transparent;
}
Color? hoverColor(BuildContext context) {
if (buttonType.isPrimary) {
return const Color(0xFF005483);
}
if (buttonType.isHighlight) {
return const Color(0xFF5C3699);
}
return null;
}
Color? fontColor(BuildContext context) {
if (buttonType.isDangerous) {
return Theme.of(context).colorScheme.error;
}
if (buttonType.isHighlight) {
return const Color(0xFF5C3699);
}
return null;
}
Color? fontHoverColor(BuildContext context) {
return Colors.white;
}
Color? borderColor(BuildContext context) {
if (buttonType.isHighlight) {
return const Color(0xFF5C3699);
}
return null;
}
}

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@ -24,13 +27,14 @@ class WorkspaceMemberBloc
extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {
WorkspaceMemberBloc({
required this.userProfile,
String? workspaceId,
this.workspace,
}) : _userBackendService = UserBackendService(userId: userProfile.id),
super(WorkspaceMemberState.initial()) {
on<WorkspaceMemberEvent>((event, emit) async {
await event.when(
initial: () async {
await _setCurrentWorkspaceId();
await _setCurrentWorkspaceId(workspaceId);
final result = await _userBackendService.getWorkspaceMembers(
_workspaceId,
@ -135,9 +139,7 @@ class WorkspaceMemberBloc
(s) => state.members.map((e) {
if (e.email == email) {
e.freeze();
return e.rebuild((p0) {
p0.role = role;
});
return e.rebuild((p0) => p0.role = role);
}
return e;
}).toList(),
@ -153,6 +155,26 @@ class WorkspaceMemberBloc
),
);
},
updateSubscriptionInfo: (info) async =>
emit(state.copyWith(subscriptionInfo: info)),
upgradePlan: () async {
final plan = state.subscriptionInfo?.plan;
if (plan == null) {
return Log.error('Failed to upgrade plan: plan is null');
}
if (plan == WorkspacePlanPB.FreePlan) {
final checkoutLink = await _userBackendService.createSubscription(
_workspaceId,
SubscriptionPlanPB.Pro,
);
checkoutLink.fold(
(pl) => afLaunchUrlString(pl.paymentLink),
(f) => Log.error('Failed to create subscription: ${f.msg}', f),
);
}
},
);
});
}
@ -178,9 +200,11 @@ class WorkspaceMemberBloc
return role;
}
Future<void> _setCurrentWorkspaceId() async {
Future<void> _setCurrentWorkspaceId(String? workspaceId) async {
if (workspace != null) {
_workspaceId = workspace!.workspaceId;
} else if (workspaceId != null && workspaceId.isNotEmpty) {
_workspaceId = workspaceId;
} else {
final currentWorkspace = await FolderEventReadCurrentWorkspace().send();
currentWorkspace.fold((s) {
@ -191,6 +215,20 @@ class WorkspaceMemberBloc
_workspaceId = '';
});
}
unawaited(_fetchWorkspaceSubscriptionInfo());
}
// We fetch workspace subscription info lazily as it's not needed in the first
// render of the page.
Future<void> _fetchWorkspaceSubscriptionInfo() async {
final result =
await UserBackendService.getWorkspaceSubscriptionInfo(_workspaceId);
result.fold(
(info) => add(WorkspaceMemberEvent.updateSubscriptionInfo(info)),
(f) => Log.error('Failed to fetch subscription info: ${f.msg}', f),
);
}
}
@ -209,6 +247,11 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent {
String email,
AFRolePB role,
) = UpdateWorkspaceMember;
const factory WorkspaceMemberEvent.updateSubscriptionInfo(
WorkspaceSubscriptionInfoPB subscriptionInfo,
) = UpdateSubscriptionInfo;
const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan;
}
enum WorkspaceMemberActionType {
@ -241,6 +284,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState {
@Default(AFRolePB.Guest) AFRolePB myRole,
@Default(null) WorkspaceMemberActionResult? actionResult,
@Default(true) bool isLoading,
@Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo,
}) = _WorkspaceMemberState;
factory WorkspaceMemberState.initial() => const WorkspaceMemberState();
@ -255,6 +299,7 @@ class WorkspaceMemberState with _$WorkspaceMemberState {
return other is WorkspaceMemberState &&
other.members == members &&
other.myRole == myRole &&
other.subscriptionInfo == subscriptionInfo &&
identical(other.actionResult, actionResult);
}
}

View File

@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
@ -20,22 +22,34 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
class WorkspaceMembersPage extends StatelessWidget {
const WorkspaceMembersPage({super.key, required this.userProfile});
const WorkspaceMembersPage({
super.key,
required this.userProfile,
required this.workspaceId,
});
final UserProfilePB userProfile;
final String workspaceId;
@override
Widget build(BuildContext context) {
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(const WorkspaceMemberEvent.initial()),
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _showResultDialog,
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_appearance_members_title.tr(),
autoSeparate: false,
children: [
if (state.myRole.canInvite) const _InviteMember(),
if (state.actionResult != null) ...[
_showMemberLimitWarning(context, state),
const VSpace(16),
],
if (state.myRole.canInvite) ...[
const _InviteMember(),
const SettingsCategorySpacer(),
],
if (state.members.isNotEmpty)
_MemberList(
members: state.members,
@ -49,62 +63,105 @@ class WorkspaceMembersPage extends StatelessWidget {
);
}
void _showResultDialog(BuildContext context, WorkspaceMemberState state) {
final actionResult = state.actionResult;
if (actionResult == null) {
return;
Widget _showMemberLimitWarning(
BuildContext context,
WorkspaceMemberState state,
) {
// We promise that state.actionResult != null before calling
// this method
final actionResult = state.actionResult!.result;
final actionType = state.actionResult!.actionType;
debugPrint("Plan: ${state.subscriptionInfo?.plan}");
if (actionType == WorkspaceMemberActionType.invite &&
actionResult.isFailure) {
final error = actionResult.getFailure().code;
if (error == ErrorCode.WorkspaceMemberLimitExceeded) {
return Row(
children: [
const FlowySvg(
FlowySvgs.warning_s,
blendMode: BlendMode.dst,
size: Size.square(20),
),
const HSpace(12),
Expanded(
child: RichText(
text: TextSpan(
children: [
if (state.subscriptionInfo?.plan ==
WorkspacePlanPB.ProPlan) ...[
TextSpan(
text: LocaleKeys
.settings_appearance_members_memberLimitExceededPro
.tr(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AFThemeExtension.of(context).strongText,
),
),
WidgetSpan(
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
// Hardcoded support email, in the future we might
// want to add this to an environment variable
onTap: () async => afLaunchUrlString(
'mailto:support@appflowy.io',
),
child: FlowyText(
LocaleKeys
.settings_appearance_members_memberLimitExceededProContact
.tr(),
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
] else ...[
TextSpan(
text: LocaleKeys
.settings_appearance_members_memberLimitExceeded
.tr(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AFThemeExtension.of(context).strongText,
),
),
WidgetSpan(
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => context
.read<WorkspaceMemberBloc>()
.add(const WorkspaceMemberEvent.upgradePlan()),
child: FlowyText(
LocaleKeys
.settings_appearance_members_memberLimitExceededUpgrade
.tr(),
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
],
],
),
),
),
],
);
}
}
final actionType = actionResult.actionType;
final result = actionResult.result;
// only show the result dialog when the action is WorkspaceMemberActionType.add
if (actionType == WorkspaceMemberActionType.add) {
result.fold(
(s) {
showSnackBarMessage(
context,
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
);
},
(f) {
Log.error('add workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
showDialog(
context: context,
builder: (context) => NavigatorOkCancelDialog(message: message),
);
},
);
} else if (actionType == WorkspaceMemberActionType.invite) {
result.fold(
(s) {
showSnackBarMessage(
context,
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
);
},
(f) {
Log.error('invite workspace member failed: $f');
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()
: LocaleKeys.settings_appearance_members_failedToInviteMember
.tr();
showDialog(
context: context,
builder: (context) => NavigatorOkCancelDialog(message: message),
);
},
);
}
result.onFailure((f) {
Log.error(
'[Member] Failed to perform ${actionType.toString()} action: $f',
);
});
return const SizedBox.shrink();
}
}

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
@ -112,26 +111,26 @@ class SettingsMenu extends StatelessWidget {
),
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.planBilling.isOn &&
userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud &&
member != null &&
member!.role.isOwner) ...[
SettingsMenuElement(
page: SettingsPage.plan,
selectedPage: currentPage,
label: LocaleKeys.settings_planPage_menuLabel.tr(),
icon: const FlowySvg(FlowySvgs.settings_plan_m),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.billing,
selectedPage: currentPage,
label: LocaleKeys.settings_billingPage_menuLabel.tr(),
icon: const FlowySvg(FlowySvgs.settings_billing_m),
changeSelectedPage: changeSelectedPage,
),
],
// if (FeatureFlag.planBilling.isOn &&
// userProfile.authenticator ==
// AuthenticatorPB.AppFlowyCloud &&
// member != null &&
// member!.role.isOwner) ...[
// SettingsMenuElement(
// page: SettingsPage.plan,
// selectedPage: currentPage,
// label: LocaleKeys.settings_planPage_menuLabel.tr(),
// icon: const FlowySvg(FlowySvgs.settings_plan_m),
// changeSelectedPage: changeSelectedPage,
// ),
// SettingsMenuElement(
// page: SettingsPage.billing,
// selectedPage: currentPage,
// label: LocaleKeys.settings_billingPage_menuLabel.tr(),
// icon: const FlowySvg(FlowySvgs.settings_billing_m),
// changeSelectedPage: changeSelectedPage,
// ),
// ],
if (kDebugMode)
SettingsMenuElement(
// no need to translate this page

View File

@ -186,6 +186,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
this.message,
this.maxWidth,
this.titleUpperCase = true,
this.autoDismiss = true,
});
final VoidCallback? onOkPressed;
@ -196,9 +197,18 @@ class NavigatorOkCancelDialog extends StatelessWidget {
final String? message;
final double? maxWidth;
final bool titleUpperCase;
final bool autoDismiss;
@override
Widget build(BuildContext context) {
final onCancel = onCancelPressed == null
? null
: () {
onCancelPressed?.call();
if (autoDismiss) {
Navigator.of(context).pop();
}
};
return StyledDialog(
maxWidth: maxWidth ?? 500,
padding: EdgeInsets.symmetric(horizontal: Insets.xl, vertical: Insets.l),
@ -227,12 +237,11 @@ class NavigatorOkCancelDialog extends StatelessWidget {
OkCancelButton(
onOkPressed: () {
onOkPressed?.call();
Navigator.of(context).pop();
},
onCancelPressed: () {
onCancelPressed?.call();
Navigator.of(context).pop();
if (autoDismiss) {
Navigator.of(context).pop();
}
},
onCancelPressed: onCancel,
okTitle: okTitle?.toUpperCase(),
cancelTitle: cancelTitle?.toUpperCase(),
),

View File

@ -73,7 +73,9 @@ Future<FlowyResult<Uint8List, Uint8List>> _extractPayload(
case FFIStatusCode.Ok:
return FlowySuccess(Uint8List.fromList(response.payload));
case FFIStatusCode.Err:
return FlowyFailure(Uint8List.fromList(response.payload));
final errorBytes = Uint8List.fromList(response.payload);
GlobalErrorCodeNotifier.receiveErrorBytes(errorBytes);
return FlowyFailure(errorBytes);
case FFIStatusCode.Internal:
final error = utf8.decode(response.payload);
Log.error("Dispatch internal error: $error");

View File

@ -1,4 +1,7 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
import 'package:flutter/foundation.dart';
class FlowyInternalError {
late FFIStatusCode _statusCode;
@ -20,15 +23,13 @@ class FlowyInternalError {
return "$_statusCode: $_error";
}
FlowyInternalError(
{required FFIStatusCode statusCode, required String error}) {
FlowyInternalError({
required FFIStatusCode statusCode,
required String error,
}) {
_statusCode = statusCode;
_error = error;
}
factory FlowyInternalError.from(FFIResponse resp) {
return FlowyInternalError(statusCode: resp.code, error: "");
}
}
class StackTraceError {
@ -48,3 +49,61 @@ class StackTraceError {
return '${error.runtimeType}. Stack trace: $trace';
}
}
typedef void ErrorListener();
class GlobalErrorCodeNotifier extends ChangeNotifier {
// Static instance with lazy initialization
static final GlobalErrorCodeNotifier _instance =
GlobalErrorCodeNotifier._internal();
FlowyError? _error;
// Private internal constructor
GlobalErrorCodeNotifier._internal();
// Factory constructor to return the same instance
factory GlobalErrorCodeNotifier() {
return _instance;
}
static void receiveError(FlowyError error) {
if (_instance._error?.code != error.code) {
_instance._error = error;
_instance.notifyListeners();
}
}
static void receiveErrorBytes(Uint8List bytes) {
try {
final error = FlowyError.fromBuffer(bytes);
if (_instance._error?.code != error.code) {
_instance._error = error;
_instance.notifyListeners();
}
} catch (e) {
Log.error("Can not parse error bytes: $e");
}
}
static ErrorListener add({
required void Function(FlowyError error) onError,
bool Function(FlowyError code)? onErrorIf,
}) {
void listener() {
final error = _instance._error;
if (error != null) {
if (onErrorIf == null || onErrorIf(error)) {
onError(error);
}
}
}
_instance.addListener(listener);
return listener;
}
static void remove(ErrorListener listener) {
_instance.removeListener(listener);
}
}

View File

@ -82,4 +82,7 @@ class Corners {
static const BorderRadius s12Border = BorderRadius.all(s12Radius);
static const Radius s12Radius = Radius.circular(12);
static const BorderRadius s16Border = BorderRadius.all(s16Radius);
static const Radius s16Radius = Radius.circular(16);
}

View File

@ -165,6 +165,7 @@ class FlowyTextButton extends StatelessWidget {
this.decoration,
this.fontFamily,
this.isDangerous = false,
this.borderColor,
});
final String text;
@ -188,6 +189,7 @@ class FlowyTextButton extends StatelessWidget {
final String? fontFamily;
final bool isDangerous;
final Color? borderColor;
@override
Widget build(BuildContext context) {
@ -211,7 +213,7 @@ class FlowyTextButton extends StatelessWidget {
child = ConstrainedBox(
constraints: constraints,
child: TextButton(
onPressed: onPressed ?? () {},
onPressed: onPressed,
focusNode: FocusNode(skipTraversal: onPressed == null),
style: ButtonStyle(
overlayColor: const WidgetStatePropertyAll(Colors.transparent),
@ -222,9 +224,10 @@ class FlowyTextButton extends StatelessWidget {
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
side: BorderSide(
color: isDangerous
? Theme.of(context).colorScheme.error
: Colors.transparent,
color: borderColor ??
(isDangerous
? Theme.of(context).colorScheme.error
: Colors.transparent),
),
borderRadius: radius ?? Corners.s6Border,
),

View File

@ -44,7 +44,7 @@ class PrimaryButton extends StatelessWidget {
return BaseStyledButton(
minWidth: mode.size.width,
minHeight: mode.size.height,
contentPadding: EdgeInsets.zero,
contentPadding: const EdgeInsets.symmetric(horizontal: 6),
bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.primaryContainer,
borderRadius: mode.borderRadius,

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"again",
"anyhow",
@ -876,7 +876,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"futures-channel",
"futures-util",
@ -1128,7 +1128,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -1153,7 +1153,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"async-trait",
@ -1417,7 +1417,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.6",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1528,7 +1528,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -2103,6 +2103,7 @@ dependencies = [
"client-api",
"collab",
"collab-entity",
"flowy-error",
"lib-infra",
]
@ -2474,12 +2475,16 @@ dependencies = [
"async-trait",
"bytes",
"chrono",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"flowy-storage-pub",
"fxhash",
"lib-infra",
"mime_guess",
"protobuf",
"serde",
"serde_json",
"tokio",
@ -2512,6 +2517,7 @@ dependencies = [
"base64 0.21.5",
"bytes",
"chrono",
"client-api",
"collab",
"collab-database",
"collab-document",
@ -3028,7 +3034,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"futures-util",
@ -3045,7 +3051,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -3477,7 +3483,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -6021,7 +6027,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",

View File

@ -53,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "76a8993cac42ff89acb507cdb99942cac7c9bfd0" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
[dependencies]
serde_json.workspace = true

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"again",
"anyhow",
@ -850,7 +850,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -862,7 +862,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"futures-channel",
"futures-util",
@ -1111,7 +1111,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -1136,7 +1136,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"async-trait",
@ -1407,7 +1407,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1518,7 +1518,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -2133,6 +2133,7 @@ dependencies = [
"client-api",
"collab",
"collab-entity",
"flowy-error",
"lib-infra",
]
@ -2504,12 +2505,16 @@ dependencies = [
"async-trait",
"bytes",
"chrono",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"flowy-storage-pub",
"fxhash",
"lib-infra",
"mime_guess",
"protobuf",
"serde",
"serde_json",
"tokio",
@ -2542,6 +2547,7 @@ dependencies = [
"base64 0.21.7",
"bytes",
"chrono",
"client-api",
"collab",
"collab-database",
"collab-document",
@ -3095,7 +3101,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"futures-util",
@ -3112,7 +3118,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -3549,7 +3555,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -6085,7 +6091,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "76a8993cac42ff89acb507cdb99942cac7c9bfd0" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
[dependencies]
serde_json.workspace = true

View File

@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31665 11.5668L12.0166 6.86683L11.0833 5.9335L7.31665 9.70016L5.41665 7.80016L4.48331 8.7335L7.31665 11.5668ZM8.24998 15.1668C7.32776 15.1668 6.46109 14.9918 5.64998 14.6418C4.83887 14.2918 4.13331 13.8168 3.53331 13.2168C2.93331 12.6168 2.45831 11.9113 2.10831 11.1002C1.75831 10.2891 1.58331 9.42238 1.58331 8.50016C1.58331 7.57794 1.75831 6.71127 2.10831 5.90016C2.45831 5.08905 2.93331 4.3835 3.53331 3.7835C4.13331 3.1835 4.83887 2.7085 5.64998 2.3585C6.46109 2.0085 7.32776 1.8335 8.24998 1.8335C9.1722 1.8335 10.0389 2.0085 10.85 2.3585C11.6611 2.7085 12.3666 3.1835 12.9666 3.7835C13.5666 4.3835 14.0416 5.08905 14.3916 5.90016C14.7416 6.71127 14.9166 7.57794 14.9166 8.50016C14.9166 9.42238 14.7416 10.2891 14.3916 11.1002C14.0416 11.9113 13.5666 12.6168 12.9666 13.2168C12.3666 13.8168 11.6611 14.2918 10.85 14.6418C10.0389 14.9918 9.1722 15.1668 8.24998 15.1668ZM8.24998 13.8335C9.73887 13.8335 11 13.3168 12.0333 12.2835C13.0666 11.2502 13.5833 9.98905 13.5833 8.50016C13.5833 7.01127 13.0666 5.75016 12.0333 4.71683C11 3.6835 9.73887 3.16683 8.24998 3.16683C6.76109 3.16683 5.49998 3.6835 4.46665 4.71683C3.43331 5.75016 2.91665 7.01127 2.91665 8.50016C2.91665 9.98905 3.43331 11.2502 4.46665 12.2835C5.49998 13.3168 6.76109 13.8335 8.24998 13.8335Z" fill="#653E8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8.00033" cy="7.99984" r="7.33333" fill="#FF811A"/>
<path d="M7.99967 4.6665C7.63148 4.6665 7.33301 4.96498 7.33301 5.33317V8.6665C7.33301 9.03469 7.63148 9.33317 7.99967 9.33317C8.36786 9.33317 8.66634 9.03469 8.66634 8.6665V5.33317C8.66634 4.96498 8.36786 4.6665 7.99967 4.6665Z" fill="white"/>
<path d="M7.99967 11.3332C8.36786 11.3332 8.66634 11.0347 8.66634 10.6665C8.66634 10.2983 8.36786 9.99984 7.99967 9.99984C7.63148 9.99984 7.33301 10.2983 7.33301 10.6665C7.33301 11.0347 7.63148 11.3332 7.99967 11.3332Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@ -358,7 +358,7 @@
"email": "ኢሜል",
"tooltipSelectIcon": "አዶን ይምረጡ",
"selectAnIcon": "አዶን ይምረጡ",
"pleaseInputYourOpenAIKey": "እባክዎን OpenAI ቁልፍዎን ያስገቡ",
"pleaseInputYourOpenAIKey": "እባክዎን AI ቁልፍዎን ያስገቡ",
"pleaseInputYourStabilityAIKey": "እባክዎ Stability AI ቁልፍን ያስገቡ",
"clickToLogout": "የአሁኑን ተጠቃሚ ለመግባት ጠቅ ያድርጉ"
},
@ -556,12 +556,12 @@
"referencedBoard": "ማጣቀሻ ቦርድ",
"referencedGrid": "ማጣቀሻ ፍርግርግ",
"referencedCalendar": "የቀን ቀን መቁጠሪያ",
"autoGeneratorMenuItemName": "OpenAI ጸሐፊ",
"autoGeneratorTitleName": "OpenAI ማንኛውንም ነገር እንዲጽፉ ይጠይቁ ...",
"autoGeneratorMenuItemName": "AI ጸሐፊ",
"autoGeneratorTitleName": "AI ማንኛውንም ነገር እንዲጽፉ ይጠይቁ ...",
"autoGeneratorLearnMore": "ተጨማሪ እወቅ",
"autoGeneratorGenerate": "ማመንጨት",
"autoGeneratorHintText": "OpenAI ይጠይቁ ...",
"autoGeneratorCantGetOpenAIKey": "የ OpenAI ቁልፍ ማግኘት አልተቻለም",
"autoGeneratorHintText": "AI ይጠይቁ ...",
"autoGeneratorCantGetOpenAIKey": "የ AI ቁልፍ ማግኘት አልተቻለም",
"autoGeneratorRewrite": "እንደገና ይፃፉ",
"smartEdit": "ረዳቶች",
"openAI": "ኦፔና",
@ -572,7 +572,7 @@
"smartEditMakeLonger": "ረዘም ላለ ጊዜ ያድርጉ",
"smartEditCouldNotFetchResult": "ከOpenAI ውጤት ማምለጥ አልተቻለም",
"smartEditCouldNotFetchKey": "ኦፕናይ ቁልፍን ማጣት አልተቻለም",
"smartEditDisabled": "በቅንብሮች ውስጥ OpenAI ያገናኙ",
"smartEditDisabled": "በቅንብሮች ውስጥ AI ያገናኙ",
"discardResponse": "የ AI ምላሾችን መጣል ይፈልጋሉ?",
"createInlineMathEquation": "እኩልነት ይፍጠሩ",
"toggleList": "የተስተካከለ ዝርዝር",
@ -657,8 +657,8 @@
"placeholder": "የምስል ዩአርኤል ያስገቡ"
},
"ai": {
"label": "ምስል OpenAI ውስጥ ምስልን ማመንጨት",
"placeholder": "ምስልን ለማመንጨት እባክዎን ለ OpenAI ይጠይቁ"
"label": "ምስል AI ውስጥ ምስልን ማመንጨት",
"placeholder": "ምስልን ለማመንጨት እባክዎን ለ AI ይጠይቁ"
},
"stability_ai": {
"label": "ምስልን Stability AI ያመነጫል",

View File

@ -406,7 +406,7 @@
"email": "بريد إلكتروني",
"tooltipSelectIcon": "حدد أيقونة",
"selectAnIcon": "حدد أيقونة",
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك",
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح AI الخاص بك",
"pleaseInputYourStabilityAIKey": "يرجى إدخال رمز Stability AI الخاص بك",
"clickToLogout": "انقر لتسجيل خروج المستخدم الحالي"
},
@ -659,23 +659,23 @@
"referencedGrid": "الشبكة المشار إليها",
"referencedCalendar": "التقويم المشار إليه",
"referencedDocument": "الوثيقة المشار إليها",
"autoGeneratorMenuItemName": "كاتب OpenAI",
"autoGeneratorTitleName": "OpenAI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...",
"autoGeneratorMenuItemName": "كاتب AI",
"autoGeneratorTitleName": "AI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...",
"autoGeneratorLearnMore": "يتعلم أكثر",
"autoGeneratorGenerate": "يولد",
"autoGeneratorHintText": "اسأل OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح OpenAI",
"autoGeneratorHintText": "اسأل AI ...",
"autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح AI",
"autoGeneratorRewrite": "اعادة كتابة",
"smartEdit": "مساعدي الذكاء الاصطناعي",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "أصلح التهجئة",
"warning": "⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.",
"smartEditSummarize": "لخص",
"smartEditImproveWriting": "تحسين الكتابة",
"smartEditMakeLonger": "اجعله أطول",
"smartEditCouldNotFetchResult": "تعذر جلب النتيجة من OpenAI",
"smartEditCouldNotFetchKey": "تعذر جلب مفتاح OpenAI",
"smartEditDisabled": "قم بتوصيل OpenAI في الإعدادات",
"smartEditCouldNotFetchResult": "تعذر جلب النتيجة من AI",
"smartEditCouldNotFetchKey": "تعذر جلب مفتاح AI",
"smartEditDisabled": "قم بتوصيل AI في الإعدادات",
"discardResponse": "هل تريد تجاهل استجابات الذكاء الاصطناعي؟",
"createInlineMathEquation": "اصنع معادلة",
"fonts": "الخطوط",
@ -770,8 +770,8 @@
"placeholder": "أدخل عنوان URL للصورة"
},
"ai": {
"label": "إنشاء صورة من OpenAI",
"placeholder": "يرجى إدخال الامر الواصف لـ OpenAI لإنشاء الصورة"
"label": "إنشاء صورة من AI",
"placeholder": "يرجى إدخال الامر الواصف لـ AI لإنشاء الصورة"
},
"stability_ai": {
"label": "إنشاء صورة من Stability AI",
@ -792,7 +792,7 @@
"label": "Unsplash"
},
"searchForAnImage": "ابحث عن صورة",
"pleaseInputYourOpenAIKey": "يرجى إدخال مفتاح OpenAI الخاص بك في صفحة الإعدادات",
"pleaseInputYourOpenAIKey": "يرجى إدخال مفتاح AI الخاص بك في صفحة الإعدادات",
"pleaseInputYourStabilityAIKey": "يرجى إدخال مفتاح Stability AI الخاص بك في صفحة الإعدادات",
"saveImageToGallery": "احفظ الصورة",
"failedToAddImageToGallery": "فشلت إضافة الصورة إلى المعرض",

View File

@ -383,7 +383,7 @@
"email": "Correu electrònic",
"tooltipSelectIcon": "Seleccioneu la icona",
"selectAnIcon": "Seleccioneu una icona",
"pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau OpenAI"
"pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau AI"
},
"mobile": {
"personalInfo": "Informació personal",
@ -602,23 +602,23 @@
"referencedBoard": "Junta de referència",
"referencedGrid": "Quadrícula de referència",
"referencedCalendar": "Calendari de referència",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Demana a AI que escrigui qualsevol cosa...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Demana a AI que escrigui qualsevol cosa...",
"autoGeneratorLearnMore": "Aprèn més",
"autoGeneratorGenerate": "Generar",
"autoGeneratorHintText": "Pregunta a OpenAI...",
"autoGeneratorCantGetOpenAIKey": "No es pot obtenir la clau OpenAI",
"autoGeneratorHintText": "Pregunta a AI...",
"autoGeneratorCantGetOpenAIKey": "No es pot obtenir la clau AI",
"autoGeneratorRewrite": "Reescriure",
"smartEdit": "Assistents d'IA",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Corregir l'ortografia",
"warning": "⚠️ Les respostes de la IA poden ser inexactes o enganyoses.",
"smartEditSummarize": "Resumir",
"smartEditImproveWriting": "Millorar l'escriptura",
"smartEditMakeLonger": "Fer més llarg",
"smartEditCouldNotFetchResult": "No s'ha pogut obtenir el resultat d'OpenAI",
"smartEditCouldNotFetchKey": "No s'ha pogut obtenir la clau OpenAI",
"smartEditDisabled": "Connecteu OpenAI a Configuració",
"smartEditCouldNotFetchResult": "No s'ha pogut obtenir el resultat d'AI",
"smartEditCouldNotFetchKey": "No s'ha pogut obtenir la clau AI",
"smartEditDisabled": "Connecteu AI a Configuració",
"discardResponse": "Voleu descartar les respostes d'IA?",
"createInlineMathEquation": "Crea una equació",
"fonts": "Fonts",
@ -714,7 +714,7 @@
"placeholder": "Introduïu l'URL de la imatge"
},
"ai": {
"label": "Generar imatge des d'OpenAI"
"label": "Generar imatge des d'AI"
},
"support": "El límit de mida de la imatge és de 5 MB. Formats admesos: JPEG, PNG, GIF, SVG",
"error": {

View File

@ -480,7 +480,7 @@
"email": "ئیمەیڵ",
"tooltipSelectIcon": "هەڵبژاەدنی وێنۆچكه‌",
"selectAnIcon": "هەڵبژاردنی وێنۆچكه‌",
"pleaseInputYourOpenAIKey": "تکایە کلیلی OpenAI ـەکەت بنووسە",
"pleaseInputYourOpenAIKey": "تکایە کلیلی AI ـەکەت بنووسە",
"pleaseInputYourStabilityAIKey": "تکایە جێگیری کلیلی AI ـەکەت بنووسە",
"clickToLogout": "بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە"
},
@ -734,23 +734,23 @@
"referencedGrid": "تۆڕی ئاماژەپێکراو",
"referencedCalendar": "ساڵنامەی ئاماژەپێکراو",
"referencedDocument": "بەڵگەنامەی ئاماژەپێکراو",
"autoGeneratorMenuItemName": "OpenAI نووسەری",
"autoGeneratorMenuItemName": "AI نووسەری",
"autoGeneratorTitleName": "داوا لە AI بکە هەر شتێک بنووسێت...",
"autoGeneratorLearnMore": "زیاتر زانین",
"autoGeneratorGenerate": "بنووسە",
"autoGeneratorHintText": "لە OpenAI پرسیار بکە...",
"autoGeneratorCantGetOpenAIKey": "نەتوانرا کلیلی OpenAI بەدەست بهێنرێت",
"autoGeneratorHintText": "لە AI پرسیار بکە...",
"autoGeneratorCantGetOpenAIKey": "نەتوانرا کلیلی AI بەدەست بهێنرێت",
"autoGeneratorRewrite": "دووبارە نووسینەوە",
"smartEdit": "یاریدەدەری زیرەک",
"openAI": "OpenAI ژیری دەستکرد",
"openAI": "AI ژیری دەستکرد",
"smartEditFixSpelling": "ڕاستکردنەوەی نووسین",
"warning": "⚠️ وەڵامەکانی AI دەتوانن هەڵە یان چەواشەکارانە بن",
"smartEditSummarize": "کورتەنووسی",
"smartEditImproveWriting": "پێشخستن نوووسین",
"smartEditMakeLonger": "درێژتری بکەرەوە",
"smartEditCouldNotFetchResult": "هیچ ئەنجامێک لە OpenAI وەرنەگیرا",
"smartEditCouldNotFetchKey": "نەتوانرا کلیلی OpenAI بهێنێتە ئاراوە",
"smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە OpenAI بکە",
"smartEditCouldNotFetchResult": "هیچ ئەنجامێک لە AI وەرنەگیرا",
"smartEditCouldNotFetchKey": "نەتوانرا کلیلی AI بهێنێتە ئاراوە",
"smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە AI بکە",
"discardResponse": "ئایا دەتەوێت وەڵامەکانی AI بسڕیتەوە؟",
"createInlineMathEquation": "درووست کردنی هاوکێشە",
"fonts": "فۆنتەکان",

View File

@ -378,7 +378,7 @@
"email": "E-mail",
"tooltipSelectIcon": "Vyberte ikonu",
"selectAnIcon": "Vyberte ikonu",
"pleaseInputYourOpenAIKey": "Prosím vložte svůj OpenAI klíč",
"pleaseInputYourOpenAIKey": "Prosím vložte svůj AI klíč",
"pleaseInputYourStabilityAIKey": "Prosím vložte svůj Stability AI klíč",
"clickToLogout": "Klin"
},
@ -606,23 +606,23 @@
"referencedGrid": "Odkazovaná mřížka",
"referencedCalendar": "Odkazovaný kalendář",
"referencedDocument": "Odkazovaný dokument",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Zeptej se AI na cokoliv...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Zeptej se AI na cokoliv...",
"autoGeneratorLearnMore": "Zjistit více",
"autoGeneratorGenerate": "Vygenerovat",
"autoGeneratorHintText": "Zeptat se OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Nepodařilo se získat klíč OpenAI",
"autoGeneratorHintText": "Zeptat se AI...",
"autoGeneratorCantGetOpenAIKey": "Nepodařilo se získat klíč AI",
"autoGeneratorRewrite": "Přepsat",
"smartEdit": "AI asistenti",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Opravit pravopis",
"warning": "⚠️ odpovědi AI mohou být nepřesné nebo zavádějící.",
"smartEditSummarize": "Shrnout",
"smartEditImproveWriting": "Vylepšit styl psaní",
"smartEditMakeLonger": "Prodloužit",
"smartEditCouldNotFetchResult": "Nepodařilo se stáhnout výsledek z OpenAI",
"smartEditCouldNotFetchKey": "Nepodařilo se stáhnout klíč OpenAI",
"smartEditDisabled": "Propojit s OpenAI v Nastavení",
"smartEditCouldNotFetchResult": "Nepodařilo se stáhnout výsledek z AI",
"smartEditCouldNotFetchKey": "Nepodařilo se stáhnout klíč AI",
"smartEditDisabled": "Propojit s AI v Nastavení",
"discardResponse": "Opravdu chcete zahodit odpovědi od AI?",
"createInlineMathEquation": "Vytvořit rovnici",
"fonts": "Písma",
@ -716,7 +716,7 @@
"placeholder": "Vlože URL adresu obrázku"
},
"ai": {
"label": "Vygenerujte obrázek pomocí OpenAI",
"label": "Vygenerujte obrázek pomocí AI",
"placeholder": "Prosím vlo"
},
"stability_ai": {
@ -735,7 +735,7 @@
"placeholder": "Vložte nebo napište odkaz na obrázek"
},
"searchForAnImage": "Hledat obrázek",
"pleaseInputYourOpenAIKey": "zadejte prosím svůj OpenAI klíč v Nastavení",
"pleaseInputYourOpenAIKey": "zadejte prosím svůj AI klíč v Nastavení",
"pleaseInputYourStabilityAIKey": "prosím vložte svůjStability AI klíč v Nastavení",
"saveImageToGallery": "Uložit obrázek",
"failedToAddImageToGallery": "Nepodařilo se přidat obrázek do galerie",

View File

@ -389,15 +389,6 @@
"loginLabel": "Anmeldung",
"logoutLabel": "Ausloggen"
},
"keys": {
"title": "KI API-Schlüssel",
"openAILabel": "OpenAI API-Schlüssel",
"openAITooltip": "Der für die KI-Modelle zu verwendende OpenAI-API-Schlüssel",
"openAIHint": "OpenAI API-Schlüssel eingeben",
"stabilityAILabel": "Stability API-Schlüssel",
"stabilityAITooltip": "Der für die KI-Modelle zu verwendende Stability API-Schlüssel",
"stabilityAIHint": "Stability API-Schlüssel eingeben"
},
"description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an."
},
"workspacePage": {
@ -642,13 +633,7 @@
"aiSettingsDescription": "Wähle oder konfiguriere KI-Modelle, die in @:appName verwendet werden. Für eine optimale Leistung empfehlen wir die Verwendung der Standardmodelloptionen",
"loginToEnableAIFeature": "KI-Funktionen werden erst nach der Anmeldung bei @:appName Cloud aktiviert. Wenn du kein @:appName-Konto hast, gehe zu „Mein Konto“, um dich zu registrieren",
"llmModel": "Sprachmodell",
"title": "KI-API-Schlüssel",
"openAILabel": "OpenAI API-Schlüssel",
"openAITooltip": "Du findest deinen geheimen API-Schlüssel auf der API-Schlüsselseite",
"openAIHint": "Gebe deinen OpenAI API-Schlüssel ein",
"stabilityAILabel": "Stability API-Schlüssel",
"stabilityAITooltip": "Dein Stability API-Schlüssel, der zur Authentifizierung deiner Anfragen verwendet wird",
"stabilityAIHint": "Gebe deinen Stability API-Schlüssel ein"
"title": "KI-API-Schlüssel"
}
},
"planPage": {
@ -1006,7 +991,7 @@
"email": "E-Mail",
"tooltipSelectIcon": "Symbol auswählen",
"selectAnIcon": "Ein Symbol auswählen",
"pleaseInputYourOpenAIKey": "Bitte gebe den OpenAI-Schlüssel ein",
"pleaseInputYourOpenAIKey": "Bitte gebe den AI-Schlüssel ein",
"pleaseInputYourStabilityAIKey": "Bitte gebe den Stability AI Schlüssel ein",
"clickToLogout": "Klicken, um den aktuellen Nutzer auszulogen"
},
@ -1343,23 +1328,23 @@
"referencedGrid": "Referenziertes Raster",
"referencedCalendar": "Referenzierter Kalender",
"referencedDocument": "Referenziertes Dokument",
"autoGeneratorMenuItemName": "OpenAI-Autor",
"autoGeneratorTitleName": "OpenAI: Die KI bitten, etwas zu schreiben ...",
"autoGeneratorMenuItemName": "AI-Autor",
"autoGeneratorTitleName": "AI: Die KI bitten, etwas zu schreiben ...",
"autoGeneratorLearnMore": "Mehr erfahren",
"autoGeneratorGenerate": "Erstellen",
"autoGeneratorHintText": "OpenAI fragen ...",
"autoGeneratorCantGetOpenAIKey": "Der OpenAI-Schlüssel kann nicht abgerufen werden",
"autoGeneratorHintText": "AI fragen ...",
"autoGeneratorCantGetOpenAIKey": "Der AI-Schlüssel kann nicht abgerufen werden",
"autoGeneratorRewrite": "Umschreiben",
"smartEdit": "KI-Assistenten",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Korrigiere Rechtschreibung",
"warning": "⚠️ KI-Antworten können ungenau oder irreführend sein.",
"smartEditSummarize": "Zusammenfassen",
"smartEditImproveWriting": "Das Geschriebene verbessern",
"smartEditMakeLonger": "Länger machen",
"smartEditCouldNotFetchResult": "Das Ergebnis konnte nicht von OpenAI abgerufen werden",
"smartEditCouldNotFetchKey": "Der OpenAI-Schlüssel konnte nicht abgerufen werden",
"smartEditDisabled": "OpenAI in den Einstellungen verbinden",
"smartEditCouldNotFetchResult": "Das Ergebnis konnte nicht von AI abgerufen werden",
"smartEditCouldNotFetchKey": "Der AI-Schlüssel konnte nicht abgerufen werden",
"smartEditDisabled": "AI in den Einstellungen verbinden",
"appflowyAIEditDisabled": "Melde dich an, um KI-Funktionen zu aktivieren",
"discardResponse": "Möchtest du die KI-Antworten verwerfen?",
"createInlineMathEquation": "Formel erstellen",
@ -1489,8 +1474,8 @@
"placeholder": "Bild-URL eingeben"
},
"ai": {
"label": "Bild mit OpenAI erstellen",
"placeholder": "Bitte den Prompt für OpenAI eingeben, um ein Bild zu erstellen"
"label": "Bild mit AI erstellen",
"placeholder": "Bitte den Prompt für AI eingeben, um ein Bild zu erstellen"
},
"stability_ai": {
"label": "Bild mit Stability AI erstellen",
@ -1512,7 +1497,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Nach einem Bild suchen",
"pleaseInputYourOpenAIKey": "biitte den OpenAI Schlüssel in der Einstellungsseite eingeben",
"pleaseInputYourOpenAIKey": "biitte den AI Schlüssel in der Einstellungsseite eingeben",
"pleaseInputYourStabilityAIKey": "biitte den Stability AI Schlüssel in der Einstellungsseite eingeben",
"saveImageToGallery": "Bild speichern",
"failedToAddImageToGallery": "Das Bild konnte nicht zur Galerie hinzugefügt werden",

View File

@ -477,7 +477,7 @@
"email": "Email",
"tooltipSelectIcon": "Select icon",
"selectAnIcon": "Select an icon",
"pleaseInputYourOpenAIKey": "παρακαλώ εισάγετε το OpenAI κλειδί σας",
"pleaseInputYourOpenAIKey": "παρακαλώ εισάγετε το AI κλειδί σας",
"pleaseInputYourStabilityAIKey": "παρακαλώ εισάγετε το Stability AI κλειδί σας",
"clickToLogout": "Κάντε κλικ για αποσύνδεση του τρέχοντος χρήστη"
},
@ -789,23 +789,23 @@
"referencedGrid": "Referenced Grid",
"referencedCalendar": "Referenced Calendar",
"referencedDocument": "Referenced Document",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Μάθετε περισσότερα",
"autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Ρωτήστε Το OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Αδυναμία λήψης κλειδιού OpenAI",
"autoGeneratorHintText": "Ρωτήστε Το AI ...",
"autoGeneratorCantGetOpenAIKey": "Αδυναμία λήψης κλειδιού AI",
"autoGeneratorRewrite": "Rewrite",
"smartEdit": "AI Assistants",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Διόρθωση ορθογραφίας",
"warning": "⚠️ Οι απαντήσεις AI μπορεί να είναι ανακριβείς ή παραπλανητικές.",
"smartEditSummarize": "Summarize",
"smartEditImproveWriting": "Improve writing",
"smartEditMakeLonger": "Make longer",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"smartEditCouldNotFetchResult": "Could not fetch result from AI",
"smartEditCouldNotFetchKey": "Could not fetch AI key",
"smartEditDisabled": "Connect AI in Settings",
"discardResponse": "Do you want to discard the AI responses?",
"createInlineMathEquation": "Create equation",
"fonts": "Γραμματοσειρές",
@ -919,8 +919,8 @@
"placeholder": "Enter image URL"
},
"ai": {
"label": "Generate image from OpenAI",
"placeholder": "Please input the prompt for OpenAI to generate image"
"label": "Generate image from AI",
"placeholder": "Please input the prompt for AI to generate image"
},
"stability_ai": {
"label": "Generate image from Stability AI",
@ -942,7 +942,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Search for an image",
"pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page",
"pleaseInputYourOpenAIKey": "please input your AI key in Settings page",
"pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page",
"saveImageToGallery": "Save image",
"failedToAddImageToGallery": "Failed to add image to gallery",

View File

@ -282,7 +282,15 @@
"removeSuccess": "Removed successfully",
"favoriteSpace": "Favorites",
"RecentSpace": "Recent",
"Spaces": "Spaces"
"Spaces": "Spaces",
"upgradeToPro": "Upgrade to Pro Plan",
"upgradeToAIMax": "Unlock unlimited AI",
"storageLimitDialogTitle": "You are running out of storage space. Upgrade to Pro Plan to get more storage",
"aiResponseLitmitDialogTitle": "You are running out of AI responses. Upgrade to Pro Plan or AI Max to get more AI responses",
"aiResponseLitmit": "You are running out of AI responses. Go to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses",
"purchaseStorageSpace": "Purchase Storage Space",
"purchaseAIResponse": "Purchase ",
"upgradeToAILocal": "AI offline on your device"
},
"notifications": {
"export": {
@ -646,14 +654,7 @@
"restartLocalAI": "Restart Local AI",
"disableLocalAIDialog": "Do you want to disable local AI?",
"localAIToggleTitle": "Toggle to enable or disable local AI",
"fetchLocalModel": "Fetch local model configuration",
"title": "AI API Keys",
"openAILabel": "OpenAI API key",
"openAITooltip": "You can find your Secret API key on the API key page",
"openAIHint": "Input your OpenAI API Key",
"stabilityAILabel": "Stability API key",
"stabilityAITooltip": "Your Stability API key, used to authenticate your requests",
"stabilityAIHint": "Input your Stability API Key"
"fetchLocalModel": "Fetch local model configuration"
}
},
"planPage": {
@ -663,14 +664,18 @@
"title": "Plan usage summary",
"storageLabel": "Storage",
"storageUsage": "{} of {} GB",
"collaboratorsLabel": "Collaborators",
"unlimitedStorageLabel": "Unlimited storage",
"collaboratorsLabel": "Members",
"collaboratorsUsage": "{} of {}",
"aiResponseLabel": "AI Responses",
"aiResponseUsage": "{} of {}",
"unlimitedAILabel": "Unlimited responses",
"proBadge": "Pro",
"memberProToggle": "Unlimited members",
"guestCollabToggle": "10 guest collaborators",
"storageUnlimited": "Unlimited storage with your Pro Plan",
"aiMaxBadge": "AI Max",
"aiOnDeviceBadge": "AI On-device",
"memberProToggle": "More members & unlimited AI",
"aiMaxToggle": "Unlimited AI responses",
"aiOnDeviceToggle": "On-device AI for ultimate privacy",
"aiCredit": {
"title": "Add @:appName AI Credit",
"price": "{}",
@ -688,25 +693,28 @@
"freeInfo": "Perfect for individuals or small teams up to 3 members.",
"proInfo": "Perfect for small and medium teams up to 10 members.",
"teamInfo": "Perfect for all productive and well-organized teams..",
"upgrade": "Compare &\n Upgrade",
"freeProOne": "Collaborative workspace",
"freeProTwo": "Up to 3 members (incl. owner)",
"freeProThree": "Unlimited guests (view-only)",
"freeProFour": "Storage 5GB",
"freeProFive": "30 day revision history",
"freeConOne": "Guest collaborators (edit access)",
"freeConTwo": "Unlimited storage",
"freeConThree": "6 month revision history",
"professionalProOne": "Collaborative workspace",
"professionalProTwo": "Unlimited members",
"professionalProThree": "Unlimited guests (view-only)",
"professionalProFour": "Unlimited storage",
"professionalProFive": "6 month revision history",
"professionalConOne": "Unlimited guest collaborators (edit access)",
"professionalConTwo": "Unlimited AI responses",
"professionalConThree": "1 year revision history",
"upgrade": "Change plan",
"canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}."
},
"addons": {
"title": "Add-ons",
"addLabel": "Add",
"activeLabel": "Added",
"aiMax": {
"title": "AI Max",
"description": "Unlock unlimited AI",
"price": "{}",
"priceInfo": "/user per month",
"billingInfo": "billed annually or {} billed monthly"
},
"aiOnDevice": {
"title": "AI On-device",
"description": "AI offline on your device",
"price": "{}",
"priceInfo": "/user per month",
"billingInfo": "billed annually or {} billed monthly"
}
},
"deal": {
"bannerLabel": "New year deal!",
"title": "Grow your team!",
@ -730,7 +738,36 @@
"title": "Payment details",
"methodLabel": "Payment method",
"methodButtonLabel": "Edit method"
}
},
"addons": {
"title": "Add-ons",
"addLabel": "Add",
"removeLabel": "Remove",
"renewLabel": "Renew",
"aiMax": {
"label": "AI Max",
"description": "Unlock unlimited AI and advanced models",
"activeDescription": "Next invoice due on {}",
"canceledDescription": "AI Max will be available until {}"
},
"aiOnDevice": {
"label": "AI On-device",
"description": "Unlock unlimited AI offline on your device",
"activeDescription": "Next invoice due on {}",
"canceledDescription": "AI On-device will be available until {}"
},
"removeDialog": {
"title": "Remove {}",
"description": "Are you sure you want to remove {plan}? You will lose access to the features and benefits of {plan} immediately."
}
},
"currentPeriodBadge": "CURRENT",
"changePeriod": "Change period",
"planPeriod": "{} period",
"monthlyInterval": "Monthly",
"monthlyPriceInfo": "per seat billed monthly",
"annualInterval": "Annually",
"annualPriceInfo": "per seat billed annually"
},
"comparePlanDialog": {
"title": "Compare & select plan",
@ -744,48 +781,44 @@
},
"freePlan": {
"title": "Free",
"description": "For organizing every corner of your work & life.",
"description": "For individuals and small groups to organize everything",
"price": "{}",
"priceInfo": "free forever"
},
"proPlan": {
"title": "Professional",
"description": "A place for small groups to plan & get organized.",
"price": "{}/month",
"priceInfo": "billed annually"
"title": "Pro",
"description": "For small teams to manage projects and team knowledge",
"price": "{}",
"priceInfo": "/user per month billed annually\n\n{} billed monthly"
},
"planLabels": {
"itemOne": "Workspaces",
"itemTwo": "Members",
"itemThree": "Guests",
"tooltipThree": "Guests have read-only permission to the specifically shared content",
"itemFour": "Guest collaborators",
"tooltipFour": "Guest collaborators are billed as one seat",
"itemFive": "Storage",
"itemSix": "Real-time collaboration",
"itemSeven": "Mobile app",
"itemEight": "AI Responses",
"tooltipEight": "Lifetime means the number of responses never reset"
"itemThree": "Storage",
"itemFour": "Real-time collaboration",
"itemFive": "Mobile app",
"itemSix": "AI Responses",
"tooltipSix": "Lifetime means the number of responses never reset",
"itemSeven": "Custom namespace",
"tooltipSeven": "Allows you to customize part of the URL for your workspace"
},
"freeLabels": {
"itemOne": "charged per workspace",
"itemTwo": "3",
"itemThree": " ",
"itemFour": "0",
"itemFive": "5 GB",
"itemSix": "yes",
"itemSeven": "yes",
"itemEight": "1,000 lifetime"
"itemTwo": "up to 3",
"itemThree": "5 GB",
"itemFour": "yes",
"itemFive": "yes",
"itemSix": "100 lifetime",
"itemSeven": ""
},
"proLabels": {
"itemOne": "charged per workspace",
"itemTwo": "up to 10",
"itemThree": " ",
"itemFour": "10 guests billed as one seat",
"itemFive": "unlimited",
"itemSix": "yes",
"itemSeven": "yes",
"itemEight": "10,000 monthly"
"itemThree": "unlimited",
"itemFour": "yes",
"itemFive": "yes",
"itemSix": "unlimited",
"itemSeven": ""
},
"paymentSuccess": {
"title": "You are now on the {} plan!",
@ -793,7 +826,7 @@
},
"downgradeDialog": {
"title": "Are you sure you want to downgrade your plan?",
"description": "Downgrading your plan will revert you back to the Free plan. Members may lose access to workspaces and you may need to free up space to meet the storage limits of the Free plan.",
"description": "Downgrading your plan will revert you back to the Free plan. Members may lose access to this workspace and you may need to free up space to meet the storage limits of the Free plan.",
"downgradeLabel": "Downgrade plan"
}
},
@ -808,7 +841,7 @@
"notifications": "Notifications",
"open": "Open Settings",
"logout": "Logout",
"logoutPrompt": "Are you sure to logout?",
"logoutPrompt": "Are you sure you want to logout?",
"selfEncryptionLogoutPrompt": "Are you sure you want to log out? Please ensure you have copied the encryption secret",
"syncSetting": "Sync Setting",
"cloudSettings": "Cloud Settings",
@ -958,7 +991,10 @@
"one": "{} member",
"other": "{} members"
},
"memberLimitExceeded": "You've reached the maximum member limit allowed for your account. If you want to add more additional members to continue your work, please request on Github",
"memberLimitExceeded": "Member limit reached, to invite more members, please ",
"memberLimitExceededUpgrade": "upgrade",
"memberLimitExceededPro": "Member limit reached, if you require more members contact ",
"memberLimitExceededProContact": "support@appflowy.io",
"failedToAddMember": "Failed to add member",
"addMemberSuccess": "Member added successfully",
"removeMember": "Remove Member",
@ -1012,7 +1048,7 @@
"email": "Email",
"tooltipSelectIcon": "Select icon",
"selectAnIcon": "Select an icon",
"pleaseInputYourOpenAIKey": "please input your OpenAI key",
"pleaseInputYourOpenAIKey": "please input your AI key",
"pleaseInputYourStabilityAIKey": "please input your Stability AI key",
"clickToLogout": "Click to logout the current user"
},
@ -1327,23 +1363,23 @@
"referencedGrid": "Referenced Grid",
"referencedCalendar": "Referenced Calendar",
"referencedDocument": "Referenced Document",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Ask OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
"autoGeneratorHintText": "Ask AI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get AI key",
"autoGeneratorRewrite": "Rewrite",
"smartEdit": "AI Assistants",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Fix spelling",
"warning": "⚠️ AI responses can be inaccurate or misleading.",
"smartEditSummarize": "Summarize",
"smartEditImproveWriting": "Improve writing",
"smartEditMakeLonger": "Make longer",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"smartEditCouldNotFetchResult": "Could not fetch result from AI",
"smartEditCouldNotFetchKey": "Could not fetch AI key",
"smartEditDisabled": "Connect AI in Settings",
"appflowyAIEditDisabled": "Sign in to enable AI features",
"discardResponse": "Do you want to discard the AI responses?",
"createInlineMathEquation": "Create equation",
@ -1473,8 +1509,8 @@
"placeholder": "Enter image URL"
},
"ai": {
"label": "Generate image from OpenAI",
"placeholder": "Please input the prompt for OpenAI to generate image"
"label": "Generate image from AI",
"placeholder": "Please input the prompt for AI to generate image"
},
"stability_ai": {
"label": "Generate image from Stability AI",
@ -1496,7 +1532,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Search for an image",
"pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page",
"pleaseInputYourOpenAIKey": "please input your AI key in Settings page",
"pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page",
"saveImageToGallery": "Save image",
"failedToAddImageToGallery": "Failed to add image to gallery",

View File

@ -361,15 +361,6 @@
"loginLabel": "Inicio de sesión",
"logoutLabel": "Cerrar sesión"
},
"keys": {
"title": "Claves API de IA",
"openAILabel": "Clave API de OpenAI",
"openAITooltip": "La clave API de OpenAI para usar en los modelos de IA",
"openAIHint": "Ingresa tu clave API de OpenAI",
"stabilityAILabel": "Clave API de Stability",
"stabilityAITooltip": "La clave API de Stability que se utilizará en los modelos de IA",
"stabilityAIHint": "Ingresa tu clave API de Stability"
},
"description": "Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta."
},
"menu": {
@ -577,7 +568,7 @@
"email": "Correo electrónico",
"tooltipSelectIcon": "Seleccionar icono",
"selectAnIcon": "Seleccione un icono",
"pleaseInputYourOpenAIKey": "por favor ingrese su clave OpenAI",
"pleaseInputYourOpenAIKey": "por favor ingrese su clave AI",
"pleaseInputYourStabilityAIKey": "por favor ingrese su clave de estabilidad AI",
"clickToLogout": "Haga clic para cerrar la sesión del usuario actual."
},
@ -902,12 +893,12 @@
"referencedGrid": "Cuadrícula referenciada",
"referencedCalendar": "Calendario referenciado",
"referencedDocument": "Documento referenciado",
"autoGeneratorMenuItemName": "Escritor de OpenAI",
"autoGeneratorTitleName": "OpenAI: Pídele a AI que escriba cualquier cosa...",
"autoGeneratorMenuItemName": "Escritor de AI",
"autoGeneratorTitleName": "AI: Pídele a AI que escriba cualquier cosa...",
"autoGeneratorLearnMore": "Aprende más",
"autoGeneratorGenerate": "Generar",
"autoGeneratorHintText": "Pregúntale a OpenAI...",
"autoGeneratorCantGetOpenAIKey": "No puedo obtener la clave de OpenAI",
"autoGeneratorHintText": "Pregúntale a AI...",
"autoGeneratorCantGetOpenAIKey": "No puedo obtener la clave de AI",
"autoGeneratorRewrite": "Volver a escribir",
"smartEdit": "Asistentes de IA",
"openAI": "IA abierta",
@ -916,9 +907,9 @@
"smartEditSummarize": "Resumir",
"smartEditImproveWriting": "Mejorar la escritura",
"smartEditMakeLonger": "hacer más largo",
"smartEditCouldNotFetchResult": "No se pudo obtener el resultado de OpenAI",
"smartEditCouldNotFetchKey": "No se pudo obtener la clave de OpenAI",
"smartEditDisabled": "Conectar OpenAI en Configuración",
"smartEditCouldNotFetchResult": "No se pudo obtener el resultado de AI",
"smartEditCouldNotFetchKey": "No se pudo obtener la clave de AI",
"smartEditDisabled": "Conectar AI en Configuración",
"discardResponse": "¿Quieres descartar las respuestas de IA?",
"createInlineMathEquation": "Crear ecuación",
"fonts": "Tipo de letra",
@ -1034,8 +1025,8 @@
"placeholder": "Introduce la URL de la imagen"
},
"ai": {
"label": "Generar imagen desde OpenAI",
"placeholder": "Ingrese el prompt para que OpenAI genere una imagen"
"label": "Generar imagen desde AI",
"placeholder": "Ingrese el prompt para que AI genere una imagen"
},
"stability_ai": {
"label": "Generar imagen desde Stability AI",
@ -1057,7 +1048,7 @@
"label": "Desempaquetar"
},
"searchForAnImage": "Buscar una imagen",
"pleaseInputYourOpenAIKey": "ingresa tu clave OpenAI en la página de Configuración",
"pleaseInputYourOpenAIKey": "ingresa tu clave AI en la página de Configuración",
"pleaseInputYourStabilityAIKey": "ingresa tu clave de Stability AI en la página de configuración",
"saveImageToGallery": "Guardar imagen",
"failedToAddImageToGallery": "No se pudo agregar la imagen a la galería",

View File

@ -273,7 +273,7 @@
"user": {
"name": "Izena",
"selectAnIcon": "Hautatu ikono bat",
"pleaseInputYourOpenAIKey": "mesedez sartu zure OpenAI gakoa"
"pleaseInputYourOpenAIKey": "mesedez sartu zure AI gakoa"
}
},
"grid": {
@ -430,23 +430,23 @@
"referencedBoard": "Erreferentziazko Batzordea",
"referencedGrid": "Erreferentziazko Sarea",
"referencedCalendar": "Erreferentziazko Egutegia",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Eskatu AIri edozer idazteko...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Eskatu AIri edozer idazteko...",
"autoGeneratorLearnMore": "Gehiago ikasi",
"autoGeneratorGenerate": "Sortu",
"autoGeneratorHintText": "Galdetu OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Ezin da lortu OpenAI gakoa",
"autoGeneratorHintText": "Galdetu AI...",
"autoGeneratorCantGetOpenAIKey": "Ezin da lortu AI gakoa",
"autoGeneratorRewrite": "Berridatzi",
"smartEdit": "AI Laguntzaileak",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Ortografia konpondu",
"warning": "⚠️ AI erantzunak okerrak edo engainagarriak izan daitezke.",
"smartEditSummarize": "Laburtu",
"smartEditImproveWriting": "Hobetu idazkera",
"smartEditMakeLonger": "Luzatu",
"smartEditCouldNotFetchResult": "Ezin izan da emaitzarik eskuratu OpenAI-tik",
"smartEditCouldNotFetchKey": "Ezin izan da OpenAI gakoa eskuratu",
"smartEditDisabled": "Konektatu OpenAI Ezarpenetan",
"smartEditCouldNotFetchResult": "Ezin izan da emaitzarik eskuratu AI-tik",
"smartEditCouldNotFetchKey": "Ezin izan da AI gakoa eskuratu",
"smartEditDisabled": "Konektatu AI Ezarpenetan",
"discardResponse": "AI erantzunak baztertu nahi dituzu?",
"createInlineMathEquation": "Sortu ekuazioa",
"toggleList": "Aldatu zerrenda",

View File

@ -296,7 +296,7 @@
"user": {
"name": "نام",
"selectAnIcon": "انتخاب یک آیکون",
"pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید",
"pleaseInputYourOpenAIKey": "لطفا کلید AI خود را وارد کنید",
"clickToLogout": "برای خروج از کاربر فعلی کلیک کنید"
},
"shortcuts": {
@ -465,23 +465,23 @@
"referencedBoard": "بورد مرجع",
"referencedGrid": "شبکه‌نمایش مرجع",
"referencedCalendar": "تقویم مرجع",
"autoGeneratorMenuItemName": "OpenAI نویسنده",
"autoGeneratorMenuItemName": "AI نویسنده",
"autoGeneratorTitleName": "از هوش مصنوعی بخواهید هر چیزی بنویسد...",
"autoGeneratorLearnMore": "بیشتر بدانید",
"autoGeneratorGenerate": "بنویس",
"autoGeneratorHintText": "از OpenAI بپرسید ...",
"autoGeneratorCantGetOpenAIKey": "کلید OpenAI را نمی توان دریافت کرد",
"autoGeneratorHintText": "از AI بپرسید ...",
"autoGeneratorCantGetOpenAIKey": "کلید AI را نمی توان دریافت کرد",
"autoGeneratorRewrite": "بازنویس",
"smartEdit": "دستیاران هوشمند",
"openAI": "OpenAI",
"openAI": "AI",
"smartEditFixSpelling": "اصلاح نگارش",
"warning": "⚠️ پاسخ‌های هوش مصنوعی می‌توانند نادرست یا گمراه‌کننده باشند",
"smartEditSummarize": "خلاصه‌نویسی",
"smartEditImproveWriting": "بهبود نگارش",
"smartEditMakeLonger": "به نوشته اضافه کن",
"smartEditCouldNotFetchResult": "نتیجه‌ای از OpenAI گرفته نشد",
"smartEditCouldNotFetchKey": "کلید OpenAI واکشی نشد",
"smartEditDisabled": "به OpenAI در تنظیمات وصل شوید",
"smartEditCouldNotFetchResult": "نتیجه‌ای از AI گرفته نشد",
"smartEditCouldNotFetchKey": "کلید AI واکشی نشد",
"smartEditDisabled": "به AI در تنظیمات وصل شوید",
"discardResponse": "آیا می خواهید پاسخ های هوش مصنوعی را حذف کنید؟",
"createInlineMathEquation": "ایجاد معادله",
"toggleList": "Toggle لیست",

View File

@ -428,7 +428,7 @@
"email": "Courriel",
"tooltipSelectIcon": "Sélectionner l'icône",
"selectAnIcon": "Sélectionnez une icône",
"pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI",
"pleaseInputYourOpenAIKey": "Veuillez entrer votre clé AI",
"pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI",
"clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel"
},
@ -701,23 +701,23 @@
"referencedGrid": "Grille référencée",
"referencedCalendar": "Calendrier référencé",
"referencedDocument": "Document référencé",
"autoGeneratorMenuItemName": "Rédacteur OpenAI",
"autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...",
"autoGeneratorMenuItemName": "Rédacteur AI",
"autoGeneratorTitleName": "AI : Demandez à l'IA d'écrire quelque chose...",
"autoGeneratorLearnMore": "Apprendre encore plus",
"autoGeneratorGenerate": "Générer",
"autoGeneratorHintText": "Demandez à OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI",
"autoGeneratorHintText": "Demandez à AI...",
"autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé AI",
"autoGeneratorRewrite": "Réécrire",
"smartEdit": "Assistants IA",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Corriger l'orthographe",
"warning": "⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.",
"smartEditSummarize": "Résumer",
"smartEditImproveWriting": "Améliorer l'écriture",
"smartEditMakeLonger": "Rallonger",
"smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'OpenAI",
"smartEditCouldNotFetchKey": "Impossible de récupérer la clé OpenAI",
"smartEditDisabled": "Connectez OpenAI dans les paramètres",
"smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'AI",
"smartEditCouldNotFetchKey": "Impossible de récupérer la clé AI",
"smartEditDisabled": "Connectez AI dans les paramètres",
"discardResponse": "Voulez-vous supprimer les réponses de l'IA ?",
"createInlineMathEquation": "Créer une équation",
"fonts": "Polices",
@ -824,8 +824,8 @@
"placeholder": "Entrez l'URL de l'image"
},
"ai": {
"label": "Générer une image à partir d'OpenAI",
"placeholder": "Veuillez saisir l'invite pour qu'OpenAI génère l'image"
"label": "Générer une image à partir d'AI",
"placeholder": "Veuillez saisir l'invite pour qu'AI génère l'image"
},
"stability_ai": {
"label": "Générer une image à partir de Stability AI",
@ -846,7 +846,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Rechercher une image",
"pleaseInputYourOpenAIKey": "veuillez saisir votre clé OpenAI dans la page Paramètres",
"pleaseInputYourOpenAIKey": "veuillez saisir votre clé AI dans la page Paramètres",
"pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres",
"saveImageToGallery": "Enregistrer l'image",
"failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie",

View File

@ -618,7 +618,7 @@
"email": "Courriel",
"tooltipSelectIcon": "Sélectionner l'icône",
"selectAnIcon": "Sélectionnez une icône",
"pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI",
"pleaseInputYourOpenAIKey": "Veuillez entrer votre clé AI",
"pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI",
"clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel"
},
@ -942,23 +942,23 @@
"referencedGrid": "Grille référencée",
"referencedCalendar": "Calendrier référencé",
"referencedDocument": "Document référencé",
"autoGeneratorMenuItemName": "Rédacteur OpenAI",
"autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...",
"autoGeneratorMenuItemName": "Rédacteur AI",
"autoGeneratorTitleName": "AI : Demandez à l'IA d'écrire quelque chose...",
"autoGeneratorLearnMore": "Apprendre encore plus",
"autoGeneratorGenerate": "Générer",
"autoGeneratorHintText": "Demandez à OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI",
"autoGeneratorHintText": "Demandez à AI...",
"autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé AI",
"autoGeneratorRewrite": "Réécrire",
"smartEdit": "Assistants IA",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Corriger l'orthographe",
"warning": "⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.",
"smartEditSummarize": "Résumer",
"smartEditImproveWriting": "Améliorer l'écriture",
"smartEditMakeLonger": "Rallonger",
"smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'OpenAI",
"smartEditCouldNotFetchKey": "Impossible de récupérer la clé OpenAI",
"smartEditDisabled": "Connectez OpenAI dans les paramètres",
"smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'AI",
"smartEditCouldNotFetchKey": "Impossible de récupérer la clé AI",
"smartEditDisabled": "Connectez AI dans les paramètres",
"discardResponse": "Voulez-vous supprimer les réponses de l'IA ?",
"createInlineMathEquation": "Créer une équation",
"fonts": "Polices",
@ -1073,8 +1073,8 @@
"placeholder": "Entrez l'URL de l'image"
},
"ai": {
"label": "Générer une image à partir d'OpenAI",
"placeholder": "Veuillez saisir l'invite pour qu'OpenAI génère l'image"
"label": "Générer une image à partir d'AI",
"placeholder": "Veuillez saisir l'invite pour qu'AI génère l'image"
},
"stability_ai": {
"label": "Générer une image à partir de Stability AI",
@ -1096,7 +1096,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Rechercher une image",
"pleaseInputYourOpenAIKey": "veuillez saisir votre clé OpenAI dans la page Paramètres",
"pleaseInputYourOpenAIKey": "veuillez saisir votre clé AI dans la page Paramètres",
"pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres",
"saveImageToGallery": "Enregistrer l'image",
"failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie",

View File

@ -333,7 +333,7 @@
"email": "ईमेल",
"tooltipSelectIcon": "आइकन चुनें",
"selectAnIcon": "एक आइकन चुनें",
"pleaseInputYourOpenAIKey": "कृपया अपनी OpenAI key इनपुट करें",
"pleaseInputYourOpenAIKey": "कृपया अपनी AI key इनपुट करें",
"clickToLogout": "वर्तमान उपयोगकर्ता को लॉगआउट करने के लिए क्लिक करें"
},
"shortcuts": {
@ -515,23 +515,23 @@
"referencedBoard": "रेफेरेंस बोर्ड",
"referencedGrid": "रेफेरेंस ग्रिड",
"referencedCalendar": "रेफेरेंस कैलेंडर",
"autoGeneratorMenuItemName": "OpenAI लेखक",
"autoGeneratorTitleName": "OpenAI: AI को कुछ भी लिखने के लिए कहें...",
"autoGeneratorMenuItemName": "AI लेखक",
"autoGeneratorTitleName": "AI: AI को कुछ भी लिखने के लिए कहें...",
"autoGeneratorLearnMore": "और जानें",
"autoGeneratorGenerate": "उत्पन्न करें",
"autoGeneratorHintText": "OpenAI से पूछें...",
"autoGeneratorCantGetOpenAIKey": "OpenAI key नहीं मिल सकी",
"autoGeneratorHintText": "AI से पूछें...",
"autoGeneratorCantGetOpenAIKey": "AI key नहीं मिल सकी",
"autoGeneratorRewrite": "पुनः लिखें",
"smartEdit": "AI सहायक",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "वर्तनी ठीक करें",
"warning": "⚠️ AI प्रतिक्रियाएँ गलत या भ्रामक हो सकती हैं।",
"smartEditSummarize": "सारांश",
"smartEditImproveWriting": "लेख में सुधार करें",
"smartEditMakeLonger": "लंबा बनाएं",
"smartEditCouldNotFetchResult": "OpenAI से परिणाम प्राप्त नहीं किया जा सका",
"smartEditCouldNotFetchKey": "OpenAI key नहीं लायी जा सकी",
"smartEditDisabled": "सेटिंग्स में OpenAI कनेक्ट करें",
"smartEditCouldNotFetchResult": "AI से परिणाम प्राप्त नहीं किया जा सका",
"smartEditCouldNotFetchKey": "AI key नहीं लायी जा सकी",
"smartEditDisabled": "सेटिंग्स में AI कनेक्ट करें",
"discardResponse": "क्या आप AI प्रतिक्रियाओं को छोड़ना चाहते हैं?",
"createInlineMathEquation": "समीकरण बनाएं",
"toggleList": "सूची टॉगल करें",

View File

@ -277,7 +277,7 @@
"user": {
"name": "Név",
"selectAnIcon": "Válasszon ki egy ikont",
"pleaseInputYourOpenAIKey": "kérjük, adja meg OpenAI kulcsát"
"pleaseInputYourOpenAIKey": "kérjük, adja meg AI kulcsát"
}
},
"grid": {
@ -432,23 +432,23 @@
"referencedBoard": "Hivatkozott feladat tábla",
"referencedGrid": "Hivatkozott táblázat",
"referencedCalendar": "Hivatkozott naptár",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Kérd meg az AI-t, hogy írjon bármit...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Kérd meg az AI-t, hogy írjon bármit...",
"autoGeneratorLearnMore": "Tudj meg többet",
"autoGeneratorGenerate": "generál",
"autoGeneratorHintText": "Kérdezd meg az OpenAI-t...",
"autoGeneratorCantGetOpenAIKey": "Nem lehet beszerezni az OpenAI kulcsot",
"autoGeneratorHintText": "Kérdezd meg az AI-t...",
"autoGeneratorCantGetOpenAIKey": "Nem lehet beszerezni az AI kulcsot",
"autoGeneratorRewrite": "Újraírni",
"smartEdit": "AI asszisztensek",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Helyesírás javítása",
"warning": "⚠️ Az AI-válaszok pontatlanok vagy félrevezetőek lehetnek.",
"smartEditSummarize": "Összesít",
"smartEditImproveWriting": "Az írás javítása",
"smartEditMakeLonger": "Hosszabb legyen",
"smartEditCouldNotFetchResult": "Nem sikerült lekérni az eredményt az OpenAI-ból",
"smartEditCouldNotFetchKey": "Nem sikerült lekérni az OpenAI kulcsot",
"smartEditDisabled": "Csatlakoztassa az OpenAI-t a Beállításokban",
"smartEditCouldNotFetchResult": "Nem sikerült lekérni az eredményt az AI-ból",
"smartEditCouldNotFetchKey": "Nem sikerült lekérni az AI kulcsot",
"smartEditDisabled": "Csatlakoztassa az AI-t a Beállításokban",
"discardResponse": "El szeretné vetni az AI-válaszokat?",
"createInlineMathEquation": "Hozzon létre egyenletet",
"toggleList": "Lista váltása",

View File

@ -376,7 +376,7 @@
"email": "Surel",
"tooltipSelectIcon": "Pilih ikon",
"selectAnIcon": "Pilih ikon",
"pleaseInputYourOpenAIKey": "silakan masukkan kunci OpenAI Anda",
"pleaseInputYourOpenAIKey": "silakan masukkan kunci AI Anda",
"pleaseInputYourStabilityAIKey": "Masukkan kunci Stability AI anda",
"clickToLogout": "Klik untuk keluar dari pengguna saat ini"
},
@ -589,23 +589,23 @@
"referencedBoard": "Papan Referensi",
"referencedGrid": "Kisi yang Direferensikan",
"referencedCalendar": "Kalender Referensi",
"autoGeneratorMenuItemName": "Penulis OpenAI",
"autoGeneratorTitleName": "OpenAI: Minta AI untuk menulis apa saja...",
"autoGeneratorMenuItemName": "Penulis AI",
"autoGeneratorTitleName": "AI: Minta AI untuk menulis apa saja...",
"autoGeneratorLearnMore": "Belajarlah lagi",
"autoGeneratorGenerate": "Menghasilkan",
"autoGeneratorHintText": "Tanya OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Tidak bisa mendapatkan kunci OpenAI",
"autoGeneratorHintText": "Tanya AI...",
"autoGeneratorCantGetOpenAIKey": "Tidak bisa mendapatkan kunci AI",
"autoGeneratorRewrite": "Menulis kembali",
"smartEdit": "Asisten AI",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Perbaiki ejaan",
"warning": "⚠️ Respons AI bisa jadi tidak akurat atau menyesatkan.",
"smartEditSummarize": "Meringkaskan",
"smartEditImproveWriting": "Perbaiki tulisan",
"smartEditMakeLonger": "Buat lebih lama",
"smartEditCouldNotFetchResult": "Tidak dapat mengambil hasil dari OpenAI",
"smartEditCouldNotFetchKey": "Tidak dapat mengambil kunci OpenAI",
"smartEditDisabled": "Hubungkan OpenAI di Pengaturan",
"smartEditCouldNotFetchResult": "Tidak dapat mengambil hasil dari AI",
"smartEditCouldNotFetchKey": "Tidak dapat mengambil kunci AI",
"smartEditDisabled": "Hubungkan AI di Pengaturan",
"discardResponse": "Apakah Anda ingin membuang respons AI?",
"createInlineMathEquation": "Buat persamaan",
"toggleList": "Beralih Daftar",
@ -690,8 +690,8 @@
"placeholder": "Masukkan URL gambar"
},
"ai": {
"label": "Buat gambar dari OpenAI",
"placeholder": "Masukkan perintah agar OpenAI menghasilkan gambar"
"label": "Buat gambar dari AI",
"placeholder": "Masukkan perintah agar AI menghasilkan gambar"
},
"stability_ai": {
"label": "Buat gambar dari Stability AI",
@ -709,7 +709,7 @@
"placeholder": "Tempel atau ketik tautan gambar"
},
"searchForAnImage": "Mencari gambar",
"pleaseInputYourOpenAIKey": "masukkan kunci OpenAI Anda di halaman Pengaturan",
"pleaseInputYourOpenAIKey": "masukkan kunci AI Anda di halaman Pengaturan",
"pleaseInputYourStabilityAIKey": "masukkan kunci AI Stabilitas Anda di halaman Pengaturan"
},
"codeBlock": {

View File

@ -427,7 +427,7 @@
"email": "E-mail",
"tooltipSelectIcon": "Seleziona l'icona",
"selectAnIcon": "Seleziona un'icona",
"pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI",
"pleaseInputYourOpenAIKey": "inserisci la tua chiave AI",
"pleaseInputYourStabilityAIKey": "per favore inserisci la tua chiave Stability AI",
"clickToLogout": "Fare clic per disconnettere l'utente corrente"
},
@ -706,23 +706,23 @@
"referencedGrid": "Griglia di riferimento",
"referencedCalendar": "Calendario referenziato",
"referencedDocument": "Documento riferito",
"autoGeneratorMenuItemName": "Scrittore OpenAI",
"autoGeneratorTitleName": "OpenAI: chiedi all'AI di scrivere qualsiasi cosa...",
"autoGeneratorMenuItemName": "Scrittore AI",
"autoGeneratorTitleName": "AI: chiedi all'AI di scrivere qualsiasi cosa...",
"autoGeneratorLearnMore": "Saperne di più",
"autoGeneratorGenerate": "creare",
"autoGeneratorHintText": "Chiedi a OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Impossibile ottenere la chiave OpenAI",
"autoGeneratorHintText": "Chiedi a AI...",
"autoGeneratorCantGetOpenAIKey": "Impossibile ottenere la chiave AI",
"autoGeneratorRewrite": "Riscrivere",
"smartEdit": "Assistenti AI",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Correggi l'ortografia",
"warning": "⚠️ Le risposte AI possono essere imprecise o fuorvianti.",
"smartEditSummarize": "Riassumere",
"smartEditImproveWriting": "Migliora la scrittura",
"smartEditMakeLonger": "Rendi più lungo",
"smartEditCouldNotFetchResult": "Impossibile recuperare il risultato da OpenAI",
"smartEditCouldNotFetchKey": "Impossibile recuperare la chiave OpenAI",
"smartEditDisabled": "Connetti OpenAI in Impostazioni",
"smartEditCouldNotFetchResult": "Impossibile recuperare il risultato da AI",
"smartEditCouldNotFetchKey": "Impossibile recuperare la chiave AI",
"smartEditDisabled": "Connetti AI in Impostazioni",
"discardResponse": "Vuoi scartare le risposte AI?",
"createInlineMathEquation": "Crea un'equazione",
"fonts": "Caratteri",
@ -831,8 +831,8 @@
"placeholder": "Inserisci l'URL dell'immagine"
},
"ai": {
"label": "Genera immagine da OpenAI",
"placeholder": "Inserisci la richiesta affinché OpenAI generi l'immagine"
"label": "Genera immagine da AI",
"placeholder": "Inserisci la richiesta affinché AI generi l'immagine"
},
"stability_ai": {
"label": "Genera immagine da Stability AI",
@ -853,7 +853,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Cerca un'immagine",
"pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI nella pagina Impostazioni",
"pleaseInputYourOpenAIKey": "inserisci la tua chiave AI nella pagina Impostazioni",
"pleaseInputYourStabilityAIKey": "inserisci la chiave Stability AI nella pagina Impostazioni",
"saveImageToGallery": "Salva immagine",
"failedToAddImageToGallery": "Impossibile aggiungere l'immagine alla galleria",

View File

@ -343,7 +343,7 @@
"user": {
"name": "名前",
"selectAnIcon": "アイコンを選択してください",
"pleaseInputYourOpenAIKey": "OpenAI キーを入力してください"
"pleaseInputYourOpenAIKey": "AI キーを入力してください"
},
"mobile": {
"username": "ユーザー名",
@ -519,23 +519,23 @@
"referencedBoard": "参照ボード",
"referencedGrid": "参照されるグリッド",
"referencedCalendar": "参照カレンダー",
"autoGeneratorMenuItemName": "OpenAI ライター",
"autoGeneratorTitleName": "OpenAI: AI に何でも書いてもらいます...",
"autoGeneratorMenuItemName": "AI ライター",
"autoGeneratorTitleName": "AI: AI に何でも書いてもらいます...",
"autoGeneratorLearnMore": "もっと詳しく知る",
"autoGeneratorGenerate": "生成",
"autoGeneratorHintText": "OpenAIに質問してください...",
"autoGeneratorCantGetOpenAIKey": "OpenAI キーを取得できません",
"autoGeneratorCantGetOpenAIKey": "AI キーを取得できません",
"autoGeneratorRewrite": "リライト",
"smartEdit": "AIアシスタント",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "スペルを修正",
"warning": "⚠️ AI の応答は不正確または誤解を招く可能性があります。",
"smartEditSummarize": "要約する",
"smartEditImproveWriting": "ライティングを改善する",
"smartEditMakeLonger": "もっと長くする",
"smartEditCouldNotFetchResult": "OpenAIから結果を取得できませんでした",
"smartEditCouldNotFetchKey": "OpenAI キーを取得できませんでした",
"smartEditDisabled": "設定で OpenAI に接続する",
"smartEditCouldNotFetchKey": "AI キーを取得できませんでした",
"smartEditDisabled": "設定で AI に接続する",
"discardResponse": "AI の応答を破棄しますか?",
"createInlineMathEquation": "方程式の作成",
"toggleList": "リストの切り替え",

View File

@ -275,7 +275,7 @@
"user": {
"name": "이름",
"selectAnIcon": "아이콘을 선택하세요",
"pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오"
"pleaseInputYourOpenAIKey": "AI 키를 입력하십시오"
}
},
"grid": {
@ -431,23 +431,23 @@
"referencedBoard": "참조 보드",
"referencedGrid": "참조된 그리드",
"referencedCalendar": "참조된 달력",
"autoGeneratorMenuItemName": "OpenAI 작성자",
"autoGeneratorTitleName": "OpenAI: AI에게 무엇이든 쓰라고 요청하세요...",
"autoGeneratorMenuItemName": "AI 작성자",
"autoGeneratorTitleName": "AI: AI에게 무엇이든 쓰라고 요청하세요...",
"autoGeneratorLearnMore": "더 알아보기",
"autoGeneratorGenerate": "생성하다",
"autoGeneratorHintText": "OpenAI에게 물어보세요 ...",
"autoGeneratorCantGetOpenAIKey": "OpenAI 키를 가져올 수 없습니다.",
"autoGeneratorCantGetOpenAIKey": "AI 키를 가져올 수 없습니다.",
"autoGeneratorRewrite": "고쳐 쓰기",
"smartEdit": "AI 어시스턴트",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "맞춤법 수정",
"warning": "⚠️ AI 응답은 부정확하거나 오해의 소지가 있을 수 있습니다.",
"smartEditSummarize": "요약하다",
"smartEditImproveWriting": "쓰기 향상",
"smartEditMakeLonger": "더 길게",
"smartEditCouldNotFetchResult": "OpenAI에서 결과를 가져올 수 없습니다.",
"smartEditCouldNotFetchKey": "OpenAI 키를 가져올 수 없습니다.",
"smartEditDisabled": "설정에서 OpenAI 연결",
"smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다.",
"smartEditDisabled": "설정에서 AI 연결",
"discardResponse": "AI 응답을 삭제하시겠습니까?",
"createInlineMathEquation": "방정식 만들기",
"toggleList": "토글 목록",

View File

@ -446,7 +446,7 @@
"email": "E-mail",
"tooltipSelectIcon": "Wybierz ikonę",
"selectAnIcon": "Wybierz ikonę",
"pleaseInputYourOpenAIKey": "wprowadź swój klucz OpenAI",
"pleaseInputYourOpenAIKey": "wprowadź swój klucz AI",
"pleaseInputYourStabilityAIKey": "wprowadź swój klucz Stability AI",
"clickToLogout": "Kliknij, aby wylogować bieżącego użytkownika"
},
@ -667,23 +667,23 @@
"referencedGrid": "Siatka referencyjna",
"referencedCalendar": "Kalendarz referencyjny",
"referencedDocument": "Dokument referencyjny",
"autoGeneratorMenuItemName": "Pisarz OpenAI",
"autoGeneratorTitleName": "OpenAI: Poproś AI o napisanie czegokolwiek...",
"autoGeneratorMenuItemName": "Pisarz AI",
"autoGeneratorTitleName": "AI: Poproś AI o napisanie czegokolwiek...",
"autoGeneratorLearnMore": "Dowiedz się więcej",
"autoGeneratorGenerate": "Generuj",
"autoGeneratorHintText": "Zapytaj OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Nie można uzyskać klucza OpenAI",
"autoGeneratorHintText": "Zapytaj AI...",
"autoGeneratorCantGetOpenAIKey": "Nie można uzyskać klucza AI",
"autoGeneratorRewrite": "Przepisz",
"smartEdit": "Asystenci AI",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Popraw pisownię",
"warning": "⚠️ Odpowiedzi AI mogą być niedokładne lub mylące.",
"smartEditSummarize": "Podsumuj",
"smartEditImproveWriting": "Popraw pisanie",
"smartEditMakeLonger": "Dłużej",
"smartEditCouldNotFetchResult": "Nie można pobrać wyniku z OpenAI",
"smartEditCouldNotFetchKey": "Nie można pobrać klucza OpenAI",
"smartEditDisabled": "Połącz OpenAI w Ustawieniach",
"smartEditCouldNotFetchResult": "Nie można pobrać wyniku z AI",
"smartEditCouldNotFetchKey": "Nie można pobrać klucza AI",
"smartEditDisabled": "Połącz AI w Ustawieniach",
"discardResponse": "Czy chcesz odrzucić odpowiedzi AI?",
"createInlineMathEquation": "Utwórz równanie",
"toggleList": "Przełącz listę",
@ -776,8 +776,8 @@
"placeholder": "Wprowadź adres URL obrazu"
},
"ai": {
"label": "Wygeneruj obraz z OpenAI",
"placeholder": "Wpisz treść podpowiedzi dla OpenAI, aby wygenerować obraz"
"label": "Wygeneruj obraz z AI",
"placeholder": "Wpisz treść podpowiedzi dla AI, aby wygenerować obraz"
},
"stability_ai": {
"label": "Wygeneruj obraz z Stability AI",
@ -795,7 +795,7 @@
"placeholder": "Wklej lub wpisz link obrazu"
},
"searchForAnImage": "Szukaj obrazu",
"pleaseInputYourOpenAIKey": "wpisz swój klucz OpenAI w ustawieniach",
"pleaseInputYourOpenAIKey": "wpisz swój klucz AI w ustawieniach",
"pleaseInputYourStabilityAIKey": "wpisz swój klucz Stability AI w ustawieniach",
"saveImageToGallery": "Zapisz obraz",
"failedToAddImageToGallery": "Nie udało się dodać obrazu do galerii",

View File

@ -421,7 +421,7 @@
"email": "E-mail",
"tooltipSelectIcon": "Selecionar ícone",
"selectAnIcon": "Escolha um ícone",
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
"pleaseInputYourOpenAIKey": "por favor insira sua chave AI",
"pleaseInputYourStabilityAIKey": "insira sua chave Stability AI",
"clickToLogout": "Clique para sair do usuário atual"
},
@ -696,18 +696,18 @@
"autoGeneratorLearnMore": "Saiba mais",
"autoGeneratorGenerate": "Gerar",
"autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...",
"autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI",
"autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da AI",
"autoGeneratorRewrite": "Reescrever",
"smartEdit": "Assistentes de IA",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Corrigir ortografia",
"warning": "⚠️ As respostas da IA podem ser imprecisas ou enganosas.",
"smartEditSummarize": "Resumir",
"smartEditImproveWriting": "melhorar a escrita",
"smartEditMakeLonger": "Faça mais",
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",
"smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI",
"smartEditDisabled": "Conecte OpenAI em Configurações",
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do AI",
"smartEditCouldNotFetchKey": "Não foi possível obter a chave AI",
"smartEditDisabled": "Conecte AI em Configurações",
"discardResponse": "Deseja descartar as respostas de IA?",
"createInlineMathEquation": "Criar equação",
"fonts": "Fontes",
@ -815,8 +815,8 @@
"placeholder": "Insira o URL da imagem"
},
"ai": {
"label": "Gerar imagem da OpenAI",
"placeholder": "Insira o prompt para OpenAI gerar imagem"
"label": "Gerar imagem da AI",
"placeholder": "Insira o prompt para AI gerar imagem"
},
"stability_ai": {
"label": "Gerar imagem da Stability AI",
@ -837,7 +837,7 @@
"label": "Remover respingo"
},
"searchForAnImage": "Procurar uma imagem",
"pleaseInputYourOpenAIKey": "insira sua chave OpenAI na página configurações",
"pleaseInputYourOpenAIKey": "insira sua chave AI na página configurações",
"pleaseInputYourStabilityAIKey": "insira sua chave Stability AI na página Configurações",
"saveImageToGallery": "Salvar imagem",
"failedToAddImageToGallery": "Falha ao adicionar imagem à galeria",

View File

@ -362,7 +362,7 @@
"email": "E-mail",
"tooltipSelectIcon": "Selecione o ícone",
"selectAnIcon": "Selecione um ícone",
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
"pleaseInputYourOpenAIKey": "por favor insira sua chave AI",
"pleaseInputYourStabilityAIKey": "por favor, insira a sua chave Stability AI",
"clickToLogout": "Clique para fazer logout"
},
@ -561,23 +561,23 @@
"referencedBoard": "Conselho Referenciado",
"referencedGrid": "grade referenciada",
"referencedCalendar": "calendário referenciado",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Peça à IA para escrever qualquer coisa...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Peça à IA para escrever qualquer coisa...",
"autoGeneratorLearnMore": "Saber mais",
"autoGeneratorGenerate": "Gerar",
"autoGeneratorHintText": "Pergunte ao OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Não é possível obter a chave OpenAI",
"autoGeneratorHintText": "Pergunte ao AI...",
"autoGeneratorCantGetOpenAIKey": "Não é possível obter a chave AI",
"autoGeneratorRewrite": "Reescrever",
"smartEdit": "Assistentes de IA",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "corrigir ortografia",
"warning": "⚠️ As respostas da IA podem ser imprecisas ou enganosas.",
"smartEditSummarize": "Resumir",
"smartEditImproveWriting": "melhorar a escrita",
"smartEditMakeLonger": "Faça mais",
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",
"smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI",
"smartEditDisabled": "Conecte OpenAI em Configurações",
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do AI",
"smartEditCouldNotFetchKey": "Não foi possível obter a chave AI",
"smartEditDisabled": "Conecte AI em Configurações",
"discardResponse": "Deseja descartar as respostas de IA?",
"createInlineMathEquation": "Criar equação",
"toggleList": "Alternar lista",
@ -662,8 +662,8 @@
"placeholder": "Insira o URL da imagem"
},
"ai": {
"label": "Gerar imagem da OpenAI",
"placeholder": "Por favor, insira o comando para a OpenAI gerar a imagem"
"label": "Gerar imagem da AI",
"placeholder": "Por favor, insira o comando para a AI gerar a imagem"
},
"stability_ai": {
"label": "Gerar imagem da Stability AI",
@ -681,7 +681,7 @@
"placeholder": "Cole ou digite uma hiperligação de imagem"
},
"searchForAnImage": "Procure uma imagem",
"pleaseInputYourOpenAIKey": "por favor, insira a sua chave OpenAI na página Configurações",
"pleaseInputYourOpenAIKey": "por favor, insira a sua chave AI na página Configurações",
"pleaseInputYourStabilityAIKey": "por favor, insira a sua chave Stability AI na página Configurações"
},
"codeBlock": {

View File

@ -999,7 +999,7 @@
"email": "Электронная почта",
"tooltipSelectIcon": "Выберите иконку",
"selectAnIcon": "Выбрать иконку",
"pleaseInputYourOpenAIKey": "Пожалуйста, введите токен OpenAI",
"pleaseInputYourOpenAIKey": "Пожалуйста, введите токен AI",
"pleaseInputYourStabilityAIKey": "Пожалуйста, введите свой токен Stability AI",
"clickToLogout": "Нажмите, чтобы выйти из текущего аккаунта"
},
@ -1326,15 +1326,15 @@
"referencedGrid": "Связанные сетки",
"referencedCalendar": "Связанные календари",
"referencedDocument": "Связанные документы",
"autoGeneratorMenuItemName": "OpenAI Генератор",
"autoGeneratorTitleName": "OpenAI: попросить ИИ написать что угодно...",
"autoGeneratorMenuItemName": "AI Генератор",
"autoGeneratorTitleName": "AI: попросить ИИ написать что угодно...",
"autoGeneratorLearnMore": "Узнать больше",
"autoGeneratorGenerate": "Генерировать",
"autoGeneratorHintText": "Спросить OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Не могу получить токен OpenAI",
"autoGeneratorHintText": "Спросить AI ...",
"autoGeneratorCantGetOpenAIKey": "Не могу получить токен AI",
"autoGeneratorRewrite": "Переписать",
"smartEdit": "ИИ-ассистенты",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Исправить правописание",
"warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.",
"smartEditSummarize": "Обобщить",
@ -1472,8 +1472,8 @@
"placeholder": "Введите URL-адрес изображения"
},
"ai": {
"label": "Сгенерировать изображение через OpenAI",
"placeholder": "Пожалуйста, введите запрос для OpenAI чтобы сгенерировать изображение"
"label": "Сгенерировать изображение через AI",
"placeholder": "Пожалуйста, введите запрос для AI чтобы сгенерировать изображение"
},
"stability_ai": {
"label": "Сгенерировать изображение через Stability AI",
@ -1495,7 +1495,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Поиск изображения",
"pleaseInputYourOpenAIKey": "пожалуйста, введите свой токен OpenAI на странице настроек",
"pleaseInputYourOpenAIKey": "пожалуйста, введите свой токен AI на странице настроек",
"pleaseInputYourStabilityAIKey": "пожалуйста, введите свой токен Stability AI на странице настроек",
"saveImageToGallery": "Сохранить изображение",
"failedToAddImageToGallery": "Ошибка добавления изображения в галерею",

View File

@ -345,7 +345,7 @@
"user": {
"name": "namn",
"selectAnIcon": "Välj en ikon",
"pleaseInputYourOpenAIKey": "vänligen ange din OpenAI-nyckel"
"pleaseInputYourOpenAIKey": "vänligen ange din AI-nyckel"
}
},
"grid": {
@ -501,23 +501,23 @@
"referencedBoard": "Refererad tavla",
"referencedGrid": "Refererade tabell",
"referencedCalendar": "Refererad kalender",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Be AI skriva vad som helst...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Be AI skriva vad som helst...",
"autoGeneratorLearnMore": "Läs mer",
"autoGeneratorGenerate": "Generera",
"autoGeneratorHintText": "Fråga OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Kan inte hämta OpenAI-nyckeln",
"autoGeneratorHintText": "Fråga AI...",
"autoGeneratorCantGetOpenAIKey": "Kan inte hämta AI-nyckeln",
"autoGeneratorRewrite": "Skriva om",
"smartEdit": "AI-assistenter",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Fixa stavningen",
"warning": "⚠️ AI-svar kan vara felaktiga eller vilseledande.",
"smartEditSummarize": "Sammanfatta",
"smartEditImproveWriting": "Förbättra skrivandet",
"smartEditMakeLonger": "Gör längre",
"smartEditCouldNotFetchResult": "Det gick inte att hämta resultatet från OpenAI",
"smartEditCouldNotFetchKey": "Det gick inte att hämta OpenAI-nyckeln",
"smartEditDisabled": "Anslut OpenAI i Inställningar",
"smartEditCouldNotFetchResult": "Det gick inte att hämta resultatet från AI",
"smartEditCouldNotFetchKey": "Det gick inte att hämta AI-nyckeln",
"smartEditDisabled": "Anslut AI i Inställningar",
"discardResponse": "Vill du kassera AI-svaren?",
"createInlineMathEquation": "Skapa ekvation",
"toggleList": "Växla lista",

View File

@ -399,7 +399,7 @@
"email": "อีเมล",
"tooltipSelectIcon": "เลือกไอคอน",
"selectAnIcon": "เลือกไอคอน",
"pleaseInputYourOpenAIKey": "โปรดระบุคีย์ OpenAI ของคุณ",
"pleaseInputYourOpenAIKey": "โปรดระบุคีย์ AI ของคุณ",
"pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณ",
"clickToLogout": "คลิกเพื่อออกจากระบบผู้ใช้ปัจจุบัน"
},
@ -647,23 +647,23 @@
"referencedGrid": "ตารางอ้างอิง",
"referencedCalendar": "ปฏิทินที่อ้างอิง",
"referencedDocument": "เอกสารอ้างอิง",
"autoGeneratorMenuItemName": "นักเขียน OpenAI",
"autoGeneratorTitleName": "OpenAI: สอบถาม AI เพื่อให้เขียนอะไรก็ได้...",
"autoGeneratorMenuItemName": "นักเขียน AI",
"autoGeneratorTitleName": "AI: สอบถาม AI เพื่อให้เขียนอะไรก็ได้...",
"autoGeneratorLearnMore": "เรียนรู้เพิ่มเติม",
"autoGeneratorGenerate": "สร้าง",
"autoGeneratorHintText": "ถาม OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "ไม่สามารถรับคีย์ OpenAI ได้",
"autoGeneratorHintText": "ถาม AI ...",
"autoGeneratorCantGetOpenAIKey": "ไม่สามารถรับคีย์ AI ได้",
"autoGeneratorRewrite": "เขียนใหม่",
"smartEdit": "ผู้ช่วย AI",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "แก้ไขการสะกด",
"warning": "⚠️ คำตอบของ AI อาจจะไม่ถูกต้องหรืออาจจะเข้าใจผิดได้",
"smartEditSummarize": "สรุป",
"smartEditImproveWriting": "ปรับปรุงการเขียน",
"smartEditMakeLonger": "ทำให้ยาวขึ้น",
"smartEditCouldNotFetchResult": "ไม่สามารถดึงผลลัพธ์จาก OpenAI ได้",
"smartEditCouldNotFetchKey": "ไม่สามารถดึงคีย์ OpenAI ได้",
"smartEditDisabled": "เชื่อมต่อ OpenAI ในการตั้งค่า",
"smartEditCouldNotFetchResult": "ไม่สามารถดึงผลลัพธ์จาก AI ได้",
"smartEditCouldNotFetchKey": "ไม่สามารถดึงคีย์ AI ได้",
"smartEditDisabled": "เชื่อมต่อ AI ในการตั้งค่า",
"discardResponse": "คุณต้องการทิ้งการตอบกลับของ AI หรือไม่",
"createInlineMathEquation": "สร้างสมการ",
"fonts": "แบบอักษร",
@ -757,8 +757,8 @@
"placeholder": "ป้อน URL รูปภาพ"
},
"ai": {
"label": "สร้างรูปภาพจาก OpenAI",
"placeholder": "โปรดระบุคำขอให้ OpenAI สร้างรูปภาพ"
"label": "สร้างรูปภาพจาก AI",
"placeholder": "โปรดระบุคำขอให้ AI สร้างรูปภาพ"
},
"stability_ai": {
"label": "สร้างรูปภาพจาก Stability AI",
@ -779,7 +779,7 @@
"label": "Unsplash"
},
"searchForAnImage": "ค้นหารูปภาพ",
"pleaseInputYourOpenAIKey": "โปรดระบุคีย์ OpenAI ของคุณในหน้าการตั้งค่า",
"pleaseInputYourOpenAIKey": "โปรดระบุคีย์ AI ของคุณในหน้าการตั้งค่า",
"pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณในหน้าการตั้งค่า",
"saveImageToGallery": "บันทึกภาพ",
"failedToAddImageToGallery": "ไม่สามารถเพิ่มรูปภาพลงในแกลเลอรี่ได้",

View File

@ -337,7 +337,7 @@
"logoutLabel": ıkış Yap"
},
"keys": {
"openAIHint": "OpenAI API Anahtarını Gir"
"openAIHint": "AI API Anahtarını Gir"
}
},
"workspacePage": {
@ -553,7 +553,7 @@
"email": "E-posta",
"tooltipSelectIcon": "Simge seç",
"selectAnIcon": "Bir simge seçin",
"pleaseInputYourOpenAIKey": "Lütfen OpenAI anahtarınızı girin",
"pleaseInputYourOpenAIKey": "Lütfen AI anahtarınızı girin",
"pleaseInputYourStabilityAIKey": "Lütfen Stability AI anahtarınızı girin",
"clickToLogout": "Geçerli kullanıcıdan çıkış yapmak için tıklayın"
},
@ -882,23 +882,23 @@
"referencedGrid": "Referans Gösterilen Tablo",
"referencedCalendar": "Referans Gösterilen Takvim",
"referencedDocument": "Referans Gösterilen Belge",
"autoGeneratorMenuItemName": "OpenAI Yazar",
"autoGeneratorTitleName": "OpenAI: AI'dan istediğinizi yazmasını isteyin...",
"autoGeneratorMenuItemName": "AI Yazar",
"autoGeneratorTitleName": "AI: AI'dan istediğinizi yazmasını isteyin...",
"autoGeneratorLearnMore": "Daha fazla bilgi edinin",
"autoGeneratorGenerate": "Oluştur",
"autoGeneratorHintText": "OpenAI'ya sorun ...",
"autoGeneratorCantGetOpenAIKey": "OpenAI anahtarı alınamıyor",
"autoGeneratorHintText": "AI'ya sorun ...",
"autoGeneratorCantGetOpenAIKey": "AI anahtarı alınamıyor",
"autoGeneratorRewrite": "Yeniden yaz",
"smartEdit": "AI Asistanları",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Yazımı düzelt",
"warning": "⚠️ AI yanıtları yanlış veya yanıltıcı olabilir.",
"smartEditSummarize": "Özetle",
"smartEditImproveWriting": "Yazımı geliştir",
"smartEditMakeLonger": "Daha uzun yap",
"smartEditCouldNotFetchResult": "OpenAI'dan sonuç alınamadı",
"smartEditCouldNotFetchKey": "OpenAI anahtarı alınamadı",
"smartEditDisabled": "Ayarlar'da OpenAI'yı bağlayın",
"smartEditCouldNotFetchResult": "AI'dan sonuç alınamadı",
"smartEditCouldNotFetchKey": "AI anahtarı alınamadı",
"smartEditDisabled": "Ayarlar'da AI'yı bağlayın",
"discardResponse": "AI yanıtlarını silmek ister misiniz?",
"createInlineMathEquation": "Denklem oluştur",
"fonts": "Yazı Tipleri",
@ -1013,8 +1013,8 @@
"placeholder": "Resim URL'sini girin"
},
"ai": {
"label": "OpenAI ile resim oluştur",
"placeholder": "Lütfen OpenAI'nin resim oluşturması için komutu girin"
"label": "AI ile resim oluştur",
"placeholder": "Lütfen AI'nin resim oluşturması için komutu girin"
},
"stability_ai": {
"label": "Stability AI ile resim oluştur",
@ -1036,7 +1036,7 @@
"label": "Unsplash"
},
"searchForAnImage": "Bir resim arayın",
"pleaseInputYourOpenAIKey": "Lütfen Ayarlar sayfasında OpenAI anahtarınızı girin",
"pleaseInputYourOpenAIKey": "Lütfen Ayarlar sayfasında AI anahtarınızı girin",
"pleaseInputYourStabilityAIKey": "Lütfen Ayarlar sayfasında Stability AI anahtarınızı girin",
"saveImageToGallery": "Resmi kaydet",
"failedToAddImageToGallery": "Resim galeriye eklenemedi",

View File

@ -351,7 +351,7 @@
"email": "Електронна пошта",
"tooltipSelectIcon": "Обрати значок",
"selectAnIcon": "Обрати значок",
"pleaseInputYourOpenAIKey": "Будь ласка, введіть ваш ключ OpenAI",
"pleaseInputYourOpenAIKey": "Будь ласка, введіть ваш ключ AI",
"clickToLogout": "Натисніть, щоб вийти з поточного облікового запису"
},
"shortcuts": {
@ -547,23 +547,23 @@
"referencedBoard": "Пов'язані дошки",
"referencedGrid": "Пов'язані сітки",
"referencedCalendar": "Календар посилань",
"autoGeneratorMenuItemName": "OpenAI Writer",
"autoGeneratorTitleName": "OpenAI: Запитайте штучний інтелект написати будь-що...",
"autoGeneratorMenuItemName": "AI Writer",
"autoGeneratorTitleName": "AI: Запитайте штучний інтелект написати будь-що...",
"autoGeneratorLearnMore": "Дізнатися більше",
"autoGeneratorGenerate": "Генерувати",
"autoGeneratorHintText": "Запитайте OpenAI...",
"autoGeneratorCantGetOpenAIKey": "Не вдається отримати ключ OpenAI",
"autoGeneratorHintText": "Запитайте AI...",
"autoGeneratorCantGetOpenAIKey": "Не вдається отримати ключ AI",
"autoGeneratorRewrite": "Переписати",
"smartEdit": "AI Асистенти",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "Виправити правопис",
"warning": "⚠️ Відповіді AI можуть бути неточними або вводити в оману.",
"smartEditSummarize": "Підсумувати",
"smartEditImproveWriting": "Покращити написання",
"smartEditMakeLonger": "Зробити довше",
"smartEditCouldNotFetchResult": "Не вдалося отримати результат від OpenAI",
"smartEditCouldNotFetchKey": "Не вдалося отримати ключ OpenAI",
"smartEditDisabled": "Підключіть OpenAI в Налаштуваннях",
"smartEditCouldNotFetchResult": "Не вдалося отримати результат від AI",
"smartEditCouldNotFetchKey": "Не вдалося отримати ключ AI",
"smartEditDisabled": "Підключіть AI в Налаштуваннях",
"discardResponse": "Ви хочете відкинути відповіді AI?",
"createInlineMathEquation": "Створити рівняння",
"toggleList": "Перемкнути список",

View File

@ -331,7 +331,7 @@
"name": "نام",
"email": "ای میل",
"selectAnIcon": "آئیکن منتخب کریں",
"pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں",
"pleaseInputYourOpenAIKey": "براہ کرم اپنی AI کی درج کریں",
"clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں"
},
"shortcuts": {
@ -511,23 +511,23 @@
"referencedBoard": "حوالہ شدہ بورڈ",
"referencedGrid": "حوالہ شدہ گرِڈ",
"referencedCalendar": "حوالہ شدہ کیلنڈر",
"autoGeneratorMenuItemName": "OpenAI رائٹر",
"autoGeneratorTitleName": "OpenAI: AI سے کچھ بھی لکھنے کے لیے کہیں...",
"autoGeneratorMenuItemName": "AI رائٹر",
"autoGeneratorTitleName": "AI: AI سے کچھ بھی لکھنے کے لیے کہیں...",
"autoGeneratorLearnMore": "مزید جانئے",
"autoGeneratorGenerate": "جنریٹ کریں",
"autoGeneratorHintText": "OpenAI سے پوچھیں...",
"autoGeneratorCantGetOpenAIKey": "OpenAI کی حاصل نہیں کر سکتا",
"autoGeneratorHintText": "AI سے پوچھیں...",
"autoGeneratorCantGetOpenAIKey": "AI کی حاصل نہیں کر سکتا",
"autoGeneratorRewrite": "دوبارہ لکھیں",
"smartEdit": "AI اسسٹنٹ",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "املا درست کریں",
"warning": "⚠️ AI کی پاسخیں غلط یا گمراہ کن ہو سکتی ہیں۔",
"smartEditSummarize": "سارے لکھیں",
"smartEditImproveWriting": "تحریر بہتر بنائیں",
"smartEditMakeLonger": "طویل تر بنائیں",
"smartEditCouldNotFetchResult": "OpenAI سے نتیجہ حاصل نہیں کر سکا",
"smartEditCouldNotFetchKey": "OpenAI کی حاصل نہیں کر سکا",
"smartEditDisabled": "Settings میں OpenAI سے منسلک کریں",
"smartEditCouldNotFetchResult": "AI سے نتیجہ حاصل نہیں کر سکا",
"smartEditCouldNotFetchKey": "AI کی حاصل نہیں کر سکا",
"smartEditDisabled": "Settings میں AI سے منسلک کریں",
"discardResponse": "کیا آپ AI کی پاسخیں حذف کرنا چاہتے ہیں؟",
"createInlineMathEquation": "مساوات بنائیں",
"toggleList": "فہرست ٹوگل کریں",

View File

@ -465,7 +465,7 @@
"email": "Email",
"tooltipSelectIcon": "Chọn biểu tượng",
"selectAnIcon": "Chọn một biểu tượng",
"pleaseInputYourOpenAIKey": "vui lòng nhập khóa OpenAI của bạn",
"pleaseInputYourOpenAIKey": "vui lòng nhập khóa AI của bạn",
"pleaseInputYourStabilityAIKey": "vui lòng nhập khóa Stability AI của bạn",
"clickToLogout": "Nhấn để đăng xuất"
},
@ -675,7 +675,7 @@
}
},
"plugins": {
"openAI": "OpenAI",
"aI": "AI",
"optionAction": {
"delete": "Xóa",
"color": "Màu",

View File

@ -376,13 +376,6 @@
"loginLabel": "登录",
"logoutLabel": "退出登录"
},
"keys": {
"title": "AI API 密钥",
"openAILabel": "OpenAI API 密钥",
"openAIHint": "输入你的 OpenAI API Key",
"stabilityAILabel": "Stability API key",
"stabilityAIHint": "输入你的 Stability API Key"
},
"description": "自定义您的简介,管理账户安全信息和 AI API keys或登陆您的账户"
},
"workspacePage": {
@ -694,7 +687,7 @@
"email": "电子邮件",
"tooltipSelectIcon": "选择图标",
"selectAnIcon": "选择一个图标",
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥",
"pleaseInputYourOpenAIKey": "请输入您的 AI 密钥",
"pleaseInputYourStabilityAIKey": "请输入您的 Stability AI 密钥",
"clickToLogout": "点击退出当前用户"
},
@ -1022,23 +1015,23 @@
"referencedGrid": "引用的网格",
"referencedCalendar": "引用的日历",
"referencedDocument": "参考文档",
"autoGeneratorMenuItemName": "OpenAI 创作",
"autoGeneratorTitleName": "OpenAI: 让 AI 写些什么...",
"autoGeneratorMenuItemName": "AI 创作",
"autoGeneratorTitleName": "AI: 让 AI 写些什么...",
"autoGeneratorLearnMore": "学习更多",
"autoGeneratorGenerate": "生成",
"autoGeneratorHintText": "让 OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "无法获得 OpenAI 密钥",
"autoGeneratorHintText": "让 AI ...",
"autoGeneratorCantGetOpenAIKey": "无法获得 AI 密钥",
"autoGeneratorRewrite": "重写",
"smartEdit": "AI 助手",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "修正拼写",
"warning": "⚠️ AI 可能不准确或具有误导性.",
"smartEditSummarize": "总结",
"smartEditImproveWriting": "提高写作水平",
"smartEditMakeLonger": "丰富内容",
"smartEditCouldNotFetchResult": "无法从 OpenAI 获取到结果",
"smartEditCouldNotFetchKey": "无法获取到 OpenAI 密钥",
"smartEditDisabled": "在设置中连接 OpenAI",
"smartEditCouldNotFetchResult": "无法从 AI 获取到结果",
"smartEditCouldNotFetchKey": "无法获取到 AI 密钥",
"smartEditDisabled": "在设置中连接 AI",
"discardResponse": "您是否要放弃 AI 继续写作?",
"createInlineMathEquation": "创建方程",
"fonts": "字体",
@ -1156,8 +1149,8 @@
"placeholder": "输入图片网址"
},
"ai": {
"label": "从 OpenAI 生成图像",
"placeholder": "请输入 OpenAI 生成图像的提示"
"label": "从 AI 生成图像",
"placeholder": "请输入 AI 生成图像的提示"
},
"stability_ai": {
"label": "从 Stability AI 生成图像",
@ -1179,7 +1172,7 @@
"label": "Unsplash"
},
"searchForAnImage": "搜索图像",
"pleaseInputYourOpenAIKey": "请在设置页面输入您的 OpenAI 密钥",
"pleaseInputYourOpenAIKey": "请在设置页面输入您的 AI 密钥",
"pleaseInputYourStabilityAIKey": "请在设置页面输入您的 Stability AI 密钥",
"saveImageToGallery": "保存图片",
"failedToAddImageToGallery": "无法将图像添加到图库",

View File

@ -333,15 +333,6 @@
"loginLabel": "登入",
"logoutLabel": "登出"
},
"keys": {
"title": "AI API 金鑰",
"openAILabel": "Open AI API 金鑰",
"openAITooltip": "以OpenAI API 金鑰使用AI 模型",
"openAIHint": "輸入您的 OpenAI API 金鑰",
"stabilityAILabel": "Stability API 金鑰",
"stabilityAITooltip": "以Stability API 金鑰使用AI 模型",
"stabilityAIHint": "輸入您的Stability API 金鑰"
},
"description": "自訂您的個人資料、管理帳戶安全性和 AI API 金鑰,或登入您的帳號"
},
"menu": {
@ -542,7 +533,7 @@
"email": "電子郵件",
"tooltipSelectIcon": "選擇圖示",
"selectAnIcon": "選擇圖示",
"pleaseInputYourOpenAIKey": "請輸入您的 OpenAI 金鑰",
"pleaseInputYourOpenAIKey": "請輸入您的 AI 金鑰",
"pleaseInputYourStabilityAIKey": "請輸入您的 Stability AI 金鑰",
"clickToLogout": "點選以登出目前使用者"
},
@ -820,23 +811,23 @@
"referencedGrid": "已連結的網格",
"referencedCalendar": "已連結的日曆",
"referencedDocument": "已連結的文件",
"autoGeneratorMenuItemName": "OpenAI 寫手",
"autoGeneratorTitleName": "OpenAI讓 AI 撰寫任何內容……",
"autoGeneratorMenuItemName": "AI 寫手",
"autoGeneratorTitleName": "AI讓 AI 撰寫任何內容……",
"autoGeneratorLearnMore": "瞭解更多",
"autoGeneratorGenerate": "產生",
"autoGeneratorHintText": "問 OpenAI……",
"autoGeneratorCantGetOpenAIKey": "無法取得 OpenAI 金鑰",
"autoGeneratorHintText": "問 AI……",
"autoGeneratorCantGetOpenAIKey": "無法取得 AI 金鑰",
"autoGeneratorRewrite": "改寫",
"smartEdit": "AI 助理",
"openAI": "OpenAI",
"aI": "AI",
"smartEditFixSpelling": "修正拼寫",
"warning": "⚠️ AI 的回覆可能不準確或具有誤導性。",
"smartEditSummarize": "總結",
"smartEditImproveWriting": "提高寫作水準",
"smartEditMakeLonger": "做得更長",
"smartEditCouldNotFetchResult": "無法取得 OpenAI 的結果",
"smartEditCouldNotFetchKey": "無法取得 OpenAI 金鑰",
"smartEditDisabled": "在設定連結 OpenAI ",
"smartEditCouldNotFetchResult": "無法取得 AI 的結果",
"smartEditCouldNotFetchKey": "無法取得 AI 金鑰",
"smartEditDisabled": "在設定連結 AI ",
"discardResponse": "確定捨棄 AI 的回覆?",
"createInlineMathEquation": "建立公式",
"fonts": "字型",
@ -951,8 +942,8 @@
"placeholder": "輸入圖片網址"
},
"ai": {
"label": "由 OpenAI 生成圖片",
"placeholder": "請輸入提示讓 OpenAI 生成圖片"
"label": "由 AI 生成圖片",
"placeholder": "請輸入提示讓 AI 生成圖片"
},
"stability_ai": {
"label": "由 Stability AI 生成圖片",
@ -974,7 +965,7 @@
"label": "Unsplash"
},
"searchForAnImage": "搜尋圖片",
"pleaseInputYourOpenAIKey": "請在設定頁面輸入您的 OpenAI 金鑰",
"pleaseInputYourOpenAIKey": "請在設定頁面輸入您的 AI 金鑰",
"pleaseInputYourStabilityAIKey": "請在設定頁面輸入您的 Stability AI 金鑰",
"saveImageToGallery": "儲存圖片",
"failedToAddImageToGallery": "無法將圖片新增到相簿",

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -718,7 +718,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"again",
"anyhow",
@ -768,7 +768,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"futures-channel",
"futures-util",
@ -823,7 +823,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"async-trait",
@ -847,7 +847,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"async-trait",
@ -877,7 +877,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"collab",
@ -897,7 +897,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"bytes",
@ -912,7 +912,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"chrono",
@ -950,7 +950,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"async-stream",
@ -989,7 +989,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bincode",
@ -1014,7 +1014,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"async-trait",
@ -1031,7 +1031,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
dependencies = [
"anyhow",
"collab",
@ -1352,7 +1352,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -1918,6 +1918,7 @@ dependencies = [
"client-api",
"collab",
"collab-entity",
"flowy-error",
"lib-infra",
]
@ -2299,12 +2300,16 @@ dependencies = [
"async-trait",
"bytes",
"chrono",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"flowy-storage-pub",
"fxhash",
"lib-infra",
"mime_guess",
"protobuf",
"rand 0.8.5",
"serde",
"serde_json",
@ -2339,6 +2344,7 @@ dependencies = [
"base64 0.21.5",
"bytes",
"chrono",
"client-api",
"collab",
"collab-database",
"collab-document",
@ -2695,7 +2701,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"futures-util",
@ -2712,7 +2718,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",
@ -3077,7 +3083,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"bytes",
@ -5223,7 +5229,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=76a8993cac42ff89acb507cdb99942cac7c9bfd0#76a8993cac42ff89acb507cdb99942cac7c9bfd0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
dependencies = [
"anyhow",
"app-error",

View File

@ -89,7 +89,7 @@ collab-document = { version = "0.2" }
collab-database = { version = "0.2" }
collab-plugins = { version = "0.2" }
collab-user = { version = "0.2" }
yrs = "0.19.1"
yrs = "0.19.2"
validator = { version = "0.16.1", features = ["derive"] }
tokio-util = "0.7.11"
zip = "2.1.3"
@ -99,8 +99,8 @@ zip = "2.1.3"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "76a8993cac42ff89acb507cdb99942cac7c9bfd0" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "76a8993cac42ff89acb507cdb99942cac7c9bfd0" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
[profile.dev]
opt-level = 0
@ -135,13 +135,13 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
# Working directory: frontend
# To update the commit ID, run:

View File

@ -226,6 +226,10 @@ impl ChatManager {
chat.index_file(file_path).await?;
Ok(())
}
pub fn local_ai_purchased(&self) {
// TODO(nathan): enable local ai
}
}
fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {

View File

@ -11,7 +11,7 @@ use lib_infra::isolate_stream::IsolateSink;
use std::sync::{Arc, Weak};
use tokio::select;
use tracing::{error, trace};
use tracing::trace;
pub struct AITools {
tasks: Arc<DashMap<String, tokio::sync::mpsc::Sender<()>>>,
@ -83,54 +83,59 @@ impl ToolTask {
pub async fn start(mut self) {
tokio::spawn(async move {
let mut sink = IsolateSink::new(Isolate::new(self.context.stream_port));
match self.cloud_service.upgrade() {
None => {},
Some(cloud_service) => {
let complete_type = match self.context.completion_type {
CompletionTypePB::UnknownCompletionType => CompletionType::ImproveWriting,
CompletionTypePB::ImproveWriting => CompletionType::ImproveWriting,
CompletionTypePB::SpellingAndGrammar => CompletionType::SpellingAndGrammar,
CompletionTypePB::MakeShorter => CompletionType::MakeShorter,
CompletionTypePB::MakeLonger => CompletionType::MakeLonger,
CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting,
};
let _ = sink.send("start:".to_string()).await;
match cloud_service
.stream_complete(&self.workspace_id, &self.context.text, complete_type)
.await
{
Ok(mut stream) => loop {
select! {
if let Some(cloud_service) = self.cloud_service.upgrade() {
let complete_type = match self.context.completion_type {
CompletionTypePB::UnknownCompletionType | CompletionTypePB::ImproveWriting => {
CompletionType::ImproveWriting
},
CompletionTypePB::SpellingAndGrammar => CompletionType::SpellingAndGrammar,
CompletionTypePB::MakeShorter => CompletionType::MakeShorter,
CompletionTypePB::MakeLonger => CompletionType::MakeLonger,
CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting,
};
let _ = sink.send("start:".to_string()).await;
match cloud_service
.stream_complete(&self.workspace_id, &self.context.text, complete_type)
.await
{
Ok(mut stream) => loop {
select! {
_ = self.stop_rx.recv() => {
return;
return;
},
result = stream.next() => {
match result {
Some(Ok(data)) => {
let s = String::from_utf8(data.to_vec()).unwrap_or_default();
trace!("stream completion data: {}", s);
let _ = sink.send(format!("data:{}", s)).await;
},
Some(Err(error)) => {
error!("stream error: {}", error);
let _ = sink.send(format!("error:{}", error)).await;
return;
},
None => {
let _ = sink.send(format!("finish:{}", self.task_id)).await;
return;
},
}
Some(Ok(data)) => {
let s = String::from_utf8(data.to_vec()).unwrap_or_default();
trace!("stream completion data: {}", s);
let _ = sink.send(format!("data:{}", s)).await;
},
Some(Err(error)) => {
handle_error(&mut sink, FlowyError::from(error)).await;
return;
},
None => {
let _ = sink.send(format!("finish:{}", self.task_id)).await;
return;
},
}
}
}
},
Err(error) => {
error!("stream complete error: {}", error);
let _ = sink.send(format!("error:{}", error)).await;
},
}
},
}
},
Err(error) => {
handle_error(&mut sink, error).await;
},
}
}
});
}
}
async fn handle_error(sink: &mut IsolateSink, error: FlowyError) {
if error.is_ai_response_limit_exceeded() {
let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await;
} else {
let _ = sink.send(format!("error:{}", error)).await;
}
}

View File

@ -66,6 +66,7 @@ dart = [
"flowy-folder/dart",
"flowy-database2/dart",
"flowy-chat/dart",
"flowy-storage/dart",
]
ts = [
"flowy-user/tauri_ts",
@ -74,6 +75,7 @@ ts = [
"flowy-database2/ts",
"flowy-config/tauri_ts",
"flowy-chat/tauri_ts",
"flowy-storage/tauri_ts",
]
openssl_vendored = ["flowy-sqlite/openssl_vendored"]

View File

@ -60,6 +60,7 @@ pub fn create_log_filter(
filters.push(format!("appflowy_local_ai={}", level));
filters.push(format!("appflowy_plugin={}", level));
filters.push(format!("flowy_ai={}", level));
filters.push(format!("flowy_storage={}", level));
// Enable the frontend logs. DO NOT DISABLE.
// These logs are essential for debugging and verifying frontend behavior.
filters.push(format!("dart_ffi={}", level));

View File

@ -418,7 +418,7 @@ impl DatabaseCloudService for ServerProvider {
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error> {
) -> FutureResult<String, FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let object_id = object_id.to_string();
@ -435,7 +435,7 @@ impl DatabaseCloudService for ServerProvider {
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
) -> FutureResult<TranslateRowResponse, Error> {
) -> FutureResult<TranslateRowResponse, FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let language = language.to_string();

View File

@ -1,10 +1,12 @@
use std::sync::Arc;
use anyhow::Context;
use client_api::entity::billing_dto::SubscriptionPlan;
use tracing::{event, trace};
use collab_entity::CollabType;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use flowy_chat::chat_manager::ChatManager;
use flowy_database2::DatabaseManager;
use flowy_document::manager::DocumentManager;
use flowy_error::{FlowyError, FlowyResult};
@ -24,6 +26,7 @@ pub(crate) struct UserStatusCallbackImpl {
pub(crate) document_manager: Arc<DocumentManager>,
pub(crate) server_provider: Arc<ServerProvider>,
pub(crate) storage_manager: Arc<StorageManager>,
pub(crate) chat_manager: Arc<ChatManager>,
}
impl UserStatusCallback for UserStatusCallbackImpl {
@ -216,4 +219,31 @@ impl UserStatusCallback for UserStatusCallbackImpl {
self.collab_builder.update_network(reachable);
self.storage_manager.update_network_reachable(reachable);
}
fn did_update_plans(&self, plans: Vec<SubscriptionPlan>) {
let mut storage_plan_changed = false;
let mut local_ai_enabled = false;
for plan in &plans {
match plan {
SubscriptionPlan::Pro | SubscriptionPlan::Team => storage_plan_changed = true,
SubscriptionPlan::AiLocal => local_ai_enabled = true,
_ => {},
}
}
if storage_plan_changed {
self.storage_manager.enable_storage_write_access();
}
if local_ai_enabled {
self.chat_manager.local_ai_purchased();
}
}
fn did_update_storage_limitation(&self, can_write: bool) {
if can_write {
self.storage_manager.enable_storage_write_access();
} else {
self.storage_manager.disable_storage_write_access();
}
}
}

View File

@ -241,6 +241,7 @@ impl AppFlowyCore {
document_manager: document_manager.clone(),
server_provider: server_provider.clone(),
storage_manager: storage_manager.clone(),
chat_manager: chat_manager.clone(),
};
let collab_interact_impl = CollabInteractImpl {

View File

@ -11,3 +11,4 @@ collab-entity = { workspace = true }
collab = { workspace = true }
anyhow.workspace = true
client-api = { workspace = true }
flowy-error = { workspace = true }

View File

@ -2,6 +2,7 @@ use anyhow::Error;
pub use client_api::entity::ai_dto::{TranslateItem, TranslateRowResponse};
use collab::core::collab::DataSource;
use collab_entity::CollabType;
use flowy_error::FlowyError;
use lib_infra::future::FutureResult;
use std::collections::HashMap;
@ -40,14 +41,14 @@ pub trait DatabaseCloudService: Send + Sync {
workspace_id: &str,
object_id: &str,
summary_row: SummaryRowContent,
) -> FutureResult<String, Error>;
) -> FutureResult<String, FlowyError>;
fn translate_database_row(
&self,
workspace_id: &str,
translate_row: TranslateRowContent,
language: &str,
) -> FutureResult<TranslateRowResponse, Error>;
) -> FutureResult<TranslateRowResponse, FlowyError>;
}
pub struct DatabaseSnapshot {

View File

@ -17,10 +17,11 @@ flowy-derive.workspace = true
flowy-notification = { workspace = true }
parking_lot.workspace = true
protobuf.workspace = true
flowy-error = { workspace = true, features = [
flowy-error = { path = "../flowy-error", features = [
"impl_from_dispatch_error",
"impl_from_collab_database",
] }
]}
lib-dispatch = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
bytes.workspace = true

View File

@ -104,6 +104,17 @@ pub struct UploadedFilePB {
pub local_file_path: String,
}
#[derive(Default, ProtoBuf, Validate)]
pub struct DownloadFilePB {
#[pb(index = 1)]
#[validate(url)]
pub url: String,
#[pb(index = 2)]
#[validate(custom = "required_valid_path")]
pub local_file_path: String,
}
#[derive(Default, ProtoBuf)]
pub struct CreateDocumentPayloadPB {
#[pb(index = 1)]

View File

@ -443,22 +443,22 @@ pub(crate) async fn upload_file_handler(
} = params.try_into_inner()?;
let manager = upgrade_document(manager)?;
let url = manager
let upload = manager
.upload_file(workspace_id, &document_id, &local_file_path)
.await?;
Ok(AFPluginData(UploadedFilePB {
url,
data_result_ok(UploadedFilePB {
url: upload.url,
local_file_path,
}))
})
}
#[instrument(level = "debug", skip_all, err)]
pub(crate) async fn download_file_handler(
params: AFPluginData<UploadedFilePB>,
params: AFPluginData<DownloadFilePB>,
manager: AFPluginState<Weak<DocumentManager>>,
) -> FlowyResult<()> {
let UploadedFilePB {
let DownloadFilePB {
url,
local_file_path,
} = params.try_into_inner()?;
@ -469,10 +469,10 @@ pub(crate) async fn download_file_handler(
// Handler for deleting file
pub(crate) async fn delete_file_handler(
params: AFPluginData<UploadedFilePB>,
params: AFPluginData<DownloadFilePB>,
manager: AFPluginState<Weak<DocumentManager>>,
) -> FlowyResult<()> {
let UploadedFilePB {
let DownloadFilePB {
url,
local_file_path,
} = params.try_into_inner()?;

View File

@ -123,9 +123,9 @@ pub enum DocumentEvent {
#[event(input = "UploadFileParamsPB", output = "UploadedFilePB")]
UploadFile = 15,
#[event(input = "UploadedFilePB")]
#[event(input = "DownloadFilePB")]
DownloadFile = 16,
#[event(input = "UploadedFilePB")]
#[event(input = "DownloadFilePB")]
DeleteFile = 17,
#[event(input = "UpdateDocumentAwarenessStatePB")]

View File

@ -20,7 +20,7 @@ use tracing::{event, instrument};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
use flowy_document_pub::cloud::DocumentCloudService;
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_storage_pub::storage::StorageService;
use flowy_storage_pub::storage::{CreatedUpload, StorageService};
use lib_dispatch::prelude::af_spawn;
use crate::document::MutexDocument;
@ -346,13 +346,12 @@ impl DocumentManager {
workspace_id: String,
document_id: &str,
local_file_path: &str,
) -> FlowyResult<String> {
) -> FlowyResult<CreatedUpload> {
let storage_service = self.storage_service_upgrade()?;
let url = storage_service
let upload = storage_service
.create_upload(&workspace_id, document_id, local_file_path)
.await?
.url;
Ok(url)
.await?;
Ok(upload)
}
pub async fn download_file(&self, local_file_path: String, url: String) -> FlowyResult<()> {

View File

@ -286,6 +286,18 @@ pub enum ErrorCode {
#[error("Local AI unavailable")]
LocalAIUnavailable = 99,
#[error("File storage limit exceeded")]
FileStorageLimitExceeded = 100,
#[error("AI Response limit exceeded")]
AIResponseLimitExceeded = 101,
#[error("Duplicate record")]
DuplicateSqliteRecord = 102,
#[error("Response timeout")]
ResponseTimeout = 103,
}
impl ErrorCode {

View File

@ -72,6 +72,14 @@ impl FlowyError {
self.code == ErrorCode::LocalVersionNotSupport
}
pub fn is_file_limit_exceeded(&self) -> bool {
self.code == ErrorCode::FileStorageLimitExceeded
}
pub fn is_ai_response_limit_exceeded(&self) -> bool {
self.code == ErrorCode::AIResponseLimitExceeded
}
static_flowy_error!(internal, ErrorCode::Internal);
static_flowy_error!(record_not_found, ErrorCode::RecordNotFound);
static_flowy_error!(workspace_initialize, ErrorCode::WorkspaceInitializeError);
@ -120,6 +128,8 @@ impl FlowyError {
static_flowy_error!(workspace_data_not_match, ErrorCode::WorkspaceDataNotMatch);
static_flowy_error!(local_ai, ErrorCode::LocalAIError);
static_flowy_error!(local_ai_unavailable, ErrorCode::LocalAIUnavailable);
static_flowy_error!(response_timeout, ErrorCode::ResponseTimeout);
static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded);
}
impl std::convert::From<ErrorCode> for FlowyError {
@ -188,3 +198,9 @@ impl From<tokio::sync::oneshot::error::RecvError> for FlowyError {
FlowyError::internal().with_context(e)
}
}
impl From<String> for FlowyError {
fn from(e: String) -> Self {
FlowyError::internal().with_context(e)
}
}

View File

@ -24,6 +24,8 @@ impl From<AppResponseError> for FlowyError {
AppErrorCode::UserUnAuthorized => ErrorCode::UserUnauthorized,
AppErrorCode::WorkspaceLimitExceeded => ErrorCode::WorkspaceLimitExceeded,
AppErrorCode::WorkspaceMemberLimitExceeded => ErrorCode::WorkspaceMemberLimitExceeded,
AppErrorCode::AIResponseLimitExceeded => ErrorCode::AIResponseLimitExceeded,
AppErrorCode::FileStorageLimitExceeded => ErrorCode::FileStorageLimitExceeded,
_ => ErrorCode::Internal,
};

View File

@ -13,7 +13,11 @@ pub mod reqwest;
#[cfg(feature = "impl_from_sqlite")]
pub mod database;
#[cfg(feature = "impl_from_collab_document")]
#[cfg(any(
feature = "impl_from_collab_document",
feature = "impl_from_collab_folder",
feature = "impl_from_collab_database"
))]
pub mod collab;
#[cfg(feature = "impl_from_collab_persistence")]

View File

@ -1098,7 +1098,7 @@ impl FolderManager {
/// Get the publish info of the view with the given view id.
/// The publish info contains the namespace and publish_name of the view.
#[tracing::instrument(level = "debug", skip(self), err)]
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult<PublishInfoResponse> {
let publish_info = self.cloud_service.get_publish_info(view_id).await?;
Ok(publish_info)

Some files were not shown because too many files have changed in this diff Show More