feat: enable removing user icon (#3487)

* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes
This commit is contained in:
Mathias Mogensen
2023-09-22 15:20:07 +02:00
committed by GitHub
parent 047f1a0b39
commit 1b966171c4
30 changed files with 392 additions and 178 deletions

View File

@ -0,0 +1,54 @@
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Settings: user icon tests', () {
testWidgets('select icon, select default option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userAvatarFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserAvatar),
);
// Open icon picker dialog
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(IconOption).first);
await tester.pumpAndSettle();
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isNotEmpty);
// Open icon picker dialog again
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Tap the default option
await tester.tap(
find.descendant(
of: find.byType(IconGallery),
matching: find.byType(UserAvatar),
),
);
await tester.pumpAndSettle();
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isEmpty);
});
});
}

View File

@ -43,6 +43,15 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
); );
}); });
}, },
removeUserIcon: () {
// Empty Icon URL = No icon
_userService.updateUserProfile(iconUrl: "").then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
},
updateUserOpenAIKey: (openAIKey) { updateUserOpenAIKey: (openAIKey) {
_userService.updateUserProfile(openAIKey: openAIKey).then((result) { _userService.updateUserProfile(openAIKey: openAIKey).then((result) {
result.fold( result.fold(
@ -105,8 +114,9 @@ class SettingsUserEvent with _$SettingsUserEvent {
const factory SettingsUserEvent.initial() = _Initial; const factory SettingsUserEvent.initial() = _Initial;
const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName;
const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail; const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail;
const factory SettingsUserEvent.updateUserIcon(String iconUrl) = const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
_UpdateUserIcon; _UpdateUserIcon;
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) = const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
_UpdateUserOpenaiKey; _UpdateUserOpenaiKey;
const factory SettingsUserEvent.didReceiveUserProfile( const factory SettingsUserEvent.didReceiveUserProfile(

View File

@ -1,11 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@ -34,7 +32,10 @@ class SidebarUser extends StatelessWidget {
builder: (context, state) => Row( builder: (context, state) => Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_buildAvatar(context, state), UserAvatar(
iconUrl: state.userProfile.iconUrl,
name: state.userProfile.name,
),
const HSpace(10), const HSpace(10),
Expanded( Expanded(
child: _buildUserName(context, state), child: _buildUserName(context, state),
@ -46,50 +47,6 @@ class SidebarUser extends StatelessWidget {
); );
} }
Widget _buildAvatar(BuildContext context, MenuUserState state) {
String iconUrl = state.userProfile.iconUrl;
if (iconUrl.isEmpty) {
iconUrl = defaultUserAvatar;
final String name = _userName(state.userProfile);
final Color color = ColorGenerator().generateColorFromString(name);
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = name
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join('');
return Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: FlowyText.semibold(
nameInitials,
color: Colors.white,
fontSize: nameInitials.length == initialsCount ? 12 : 14,
),
);
}
return SizedBox.square(
dimension: 25,
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: FlowySvg(
FlowySvgData('emoji/$iconUrl'),
blendMode: null,
),
),
),
);
}
Widget _buildUserName(BuildContext context, MenuUserState state) { Widget _buildUserName(BuildContext context, MenuUserState state) {
final String name = _userName(state.userProfile); final String name = _userName(state.userProfile);
return FlowyText.medium( return FlowyText.medium(

View File

@ -9,10 +9,14 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/flowy_tooltip.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -49,21 +53,16 @@ class SettingsUserView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_renderUserNameInput(context), _buildUserIconSetting(context),
if (isSupabaseEnabled) ...[ if (isSupabaseEnabled) ...[
const VSpace(20), const VSpace(12),
UserEmailInput(user.email) UserEmailInput(user.email)
], ],
const VSpace(12),
const VSpace(20),
_renderCurrentIcon(context),
const VSpace(20),
_renderCurrentOpenaiKey(context), _renderCurrentOpenaiKey(context),
const VSpace(20), const VSpace(12),
// _renderHistoricalUser(context),
_renderLoginOrLogoutButton(context, state), _renderLoginOrLogoutButton(context, state),
const VSpace(20), const VSpace(12),
], ],
), ),
), ),
@ -71,6 +70,115 @@ class SettingsUserView extends StatelessWidget {
); );
} }
Row _buildUserIconSetting(BuildContext context) {
return Row(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showIconPickerDialog(context),
child: FlowyHover(
style: const HoverStyle.transparent(),
builder: (context, onHover) {
Widget avatar = UserAvatar(
iconUrl: user.iconUrl,
name: user.name,
isLarge: true,
);
if (onHover) {
avatar = _avatarOverlay(
context: context,
hasIcon: user.iconUrl.isNotEmpty,
child: avatar,
);
}
return avatar;
},
),
),
const HSpace(12),
Flexible(child: _renderUserNameInput(context)),
],
);
}
Future<void> _showIconPickerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (dialogContext) => SimpleDialog(
title: FlowyText.medium(
LocaleKeys.settings_user_selectAnIcon.tr(),
fontSize: FontSizes.s16,
),
children: [
SizedBox(
height: 300,
width: 300,
child: IconGallery(
defaultOption: _defaultIconOption(context),
selectedIcon: user.iconUrl,
onSelectIcon: (iconUrl, isSelected) {
if (isSelected) {
return Navigator.of(context).pop();
}
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: iconUrl));
Navigator.of(context).pop();
},
),
),
],
),
);
}
// Returns a Widget that is the Default Option for the
// Icon Gallery, enabling users to choose the auto-generated
// icon again.
Widget _defaultIconOption(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
context
.read<SettingsUserViewBloc>()
.add(const SettingsUserEvent.removeUserIcon());
Navigator.of(context).pop();
},
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: user.iconUrl.isEmpty
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
child: FlowyHover(
style: HoverStyle(
hoverColor: user.iconUrl.isEmpty
? Colors.transparent
: Theme.of(context).colorScheme.tertiaryContainer,
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: DecoratedBox(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: UserAvatar(
iconUrl: "",
name: user.name,
isLarge: true,
),
),
),
),
),
);
}
/// Renders either a login or logout button based on the user's authentication status, or nothing if Supabase is not enabled. /// Renders either a login or logout button based on the user's authentication status, or nothing if Supabase is not enabled.
/// ///
/// This function checks the current user's authentication type and Supabase /// This function checks the current user's authentication type and Supabase
@ -86,9 +194,7 @@ class SettingsUserView extends StatelessWidget {
// If the user is logged in locally, render a third-party login button. // If the user is logged in locally, render a third-party login button.
if (state.userProfile.authType == AuthTypePB.Local) { if (state.userProfile.authType == AuthTypePB.Local) {
return SettingThirdPartyLogin( return SettingThirdPartyLogin(didLogin: didLogin);
didLogin: didLogin,
);
} }
return SettingLogoutButton(user: user, didLogout: didLogout); return SettingLogoutButton(user: user, didLogout: didLogout);
@ -100,20 +206,49 @@ class SettingsUserView extends StatelessWidget {
return UserNameInput(name); return UserNameInput(name);
} }
Widget _renderCurrentIcon(BuildContext context) {
String iconUrl =
context.read<SettingsUserViewBloc>().state.userProfile.iconUrl;
if (iconUrl.isEmpty) {
iconUrl = defaultUserAvatar;
}
return _CurrentIcon(iconUrl);
}
Widget _renderCurrentOpenaiKey(BuildContext context) { Widget _renderCurrentOpenaiKey(BuildContext context) {
final String openAIKey = final String openAIKey =
context.read<SettingsUserViewBloc>().state.userProfile.openaiKey; context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
return _OpenaiKeyInput(openAIKey); return _OpenaiKeyInput(openAIKey);
} }
Widget _avatarOverlay({
required BuildContext context,
required bool hasIcon,
required Widget child,
}) =>
FlowyTooltip.delayedTooltip(
message: LocaleKeys.settings_user_tooltipSelectIcon.tr(),
child: Stack(
children: [
Container(
width: 56,
height: 56,
foregroundDecoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(hasIcon ? 0.8 : 0.5),
shape: BoxShape.circle,
),
child: child,
),
const Positioned(
top: 0,
left: 0,
bottom: 0,
right: 0,
child: Center(
child: SizedBox(
width: 32,
height: 32,
child: FlowySvg(FlowySvgs.emoji_s),
),
),
),
],
),
);
} }
@visibleForTesting @visibleForTesting
@ -317,69 +452,19 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
} }
} }
class _CurrentIcon extends StatelessWidget { typedef SelectIconCallback = void Function(String iconUrl, bool isSelected);
final String iconUrl;
const _CurrentIcon(this.iconUrl, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
void setIcon(String iconUrl) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl));
Navigator.of(context).pop();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
LocaleKeys.settings_user_icon.tr(),
style: Theme.of(context).textTheme.titleSmall!.copyWith(
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
InkWell(
borderRadius: Corners.s6Border,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: FlowyText.medium(
LocaleKeys.settings_user_selectAnIcon.tr(),
fontSize: FontSizes.s16,
),
children: [
SizedBox(
height: 300,
width: 300,
child: IconGallery(setIcon),
)
],
);
},
);
},
child: Container(
margin: const EdgeInsets.fromLTRB(0, 5, 5, 5),
child: FlowySvg(
FlowySvgData('emoji/$iconUrl'),
size: _iconSize,
blendMode: null,
),
),
),
],
);
}
}
class IconGallery extends StatelessWidget { class IconGallery extends StatelessWidget {
final Function setIcon; final String selectedIcon;
const IconGallery(this.setIcon, {Key? key}) : super(key: key); final SelectIconCallback onSelectIcon;
final Widget? defaultOption;
const IconGallery({
super.key,
required this.selectedIcon,
required this.onSelectIcon,
this.defaultOption,
});
Future<List<String>> _getIcons(BuildContext context) async { Future<List<String>> _getIcons(BuildContext context) async {
final manifestContent = final manifestContent =
@ -403,23 +488,29 @@ class IconGallery extends StatelessWidget {
return FutureBuilder<List<String>>( return FutureBuilder<List<String>>(
future: _getIcons(context), future: _getIcons(context),
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) { builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData) { if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return GridView.count( return GridView.count(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
crossAxisCount: 5, crossAxisCount: 5,
children: (snapshot.data ?? []).map((String iconUrl) { mainAxisSpacing: 4,
return IconOption( crossAxisSpacing: 4,
FlowySvgData('emoji/$iconUrl'), children: [
iconUrl, if (defaultOption != null) defaultOption!,
setIcon, ...snapshot.data!
); .mapIndexed(
}).toList(), (int index, String iconUrl) => IconOption(
); emoji: FlowySvgData('emoji/$iconUrl'),
} else { iconUrl: iconUrl,
return const Center( onSelectIcon: onSelectIcon,
child: CircularProgressIndicator(), isSelected: iconUrl == selectedIcon,
),
)
.toList(),
],
); );
} }
return const Center(child: CircularProgressIndicator());
}, },
); );
} }
@ -428,21 +519,34 @@ class IconGallery extends StatelessWidget {
class IconOption extends StatelessWidget { class IconOption extends StatelessWidget {
final FlowySvgData emoji; final FlowySvgData emoji;
final String iconUrl; final String iconUrl;
final Function setIcon; final SelectIconCallback onSelectIcon;
final bool isSelected;
IconOption(this.emoji, this.iconUrl, this.setIcon, {Key? key}) IconOption({
: super(key: ValueKey(emoji)); required this.emoji,
required this.iconUrl,
required this.onSelectIcon,
required this.isSelected,
}) : super(key: ValueKey(emoji));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return InkWell(
borderRadius: Corners.s6Border, borderRadius: Corners.s8Border,
hoverColor: Theme.of(context).colorScheme.tertiaryContainer, hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
onTap: () => setIcon(iconUrl), onTap: () => onSelectIcon(iconUrl, isSelected),
child: FlowySvg( child: DecoratedBox(
emoji, decoration: BoxDecoration(
size: _iconSize, color: isSelected
blendMode: null, ? Theme.of(context).colorScheme.primary
: Colors.transparent,
borderRadius: Corners.s8Border,
),
child: FlowySvg(
emoji,
size: _iconSize,
blendMode: null,
),
), ),
); );
} }

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
const _tooltipWaitDuration = Duration(milliseconds: 300);
class FlowyTooltip {
static Tooltip delayedTooltip({
String? message,
InlineSpan? richMessage,
bool? preferBelow,
Widget? child,
}) {
return Tooltip(
waitDuration: _tooltipWaitDuration,
message: message,
richMessage: richMessage,
preferBelow: preferBelow,
child: child,
);
}
}

View File

@ -0,0 +1,82 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
const double _smallSize = 28;
const double _largeSize = 56;
class UserAvatar extends StatelessWidget {
const UserAvatar({
super.key,
required this.iconUrl,
required this.name,
this.isLarge = false,
});
final String iconUrl;
final String name;
final bool isLarge;
@override
Widget build(BuildContext context) {
final size = isLarge ? _largeSize : _smallSize;
if (iconUrl.isEmpty) {
final String nameOrDefault = _userName(name);
final Color color = ColorGenerator().generateColorFromString(name);
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = nameOrDefault
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join('');
return Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: FlowyText.semibold(
nameInitials,
color: Colors.white,
fontSize: isLarge
? nameInitials.length == initialsCount
? 20
: 26
: nameInitials.length == initialsCount
? 12
: 14,
),
);
}
return SizedBox.square(
dimension: size,
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: FlowySvg(
FlowySvgData('emoji/$iconUrl'),
blendMode: null,
),
),
),
);
}
/// Return the user name, if the user name is empty,
/// return the default user name.
String _userName(String name) =>
name.isEmpty ? LocaleKeys.defaultUsername.tr() : name;
}

View File

@ -118,6 +118,15 @@ class HoverStyle {
this.hoverColor, this.hoverColor,
this.foregroundColorOnHover, this.foregroundColorOnHover,
}); });
const HoverStyle.transparent({
this.borderColor = Colors.transparent,
this.borderWidth = 0,
this.borderRadius = const BorderRadius.all(Radius.circular(6)),
this.contentMargin = EdgeInsets.zero,
this.backgroundColor = Colors.transparent,
this.foregroundColorOnHover,
}) : hoverColor = Colors.transparent;
} }
class FlowyHoverContainer extends StatelessWidget { class FlowyHoverContainer extends StatelessWidget {

View File

@ -258,7 +258,6 @@
}, },
"user": { "user": {
"name": "اسم", "name": "اسم",
"icon": "أيقونة",
"selectAnIcon": "حدد أيقونة", "selectAnIcon": "حدد أيقونة",
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك" "pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
} }

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "Nom", "name": "Nom",
"icon": "Icona",
"selectAnIcon": "Seleccioneu una icona", "selectAnIcon": "Seleccioneu una icona",
"pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau OpenAI" "pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau OpenAI"
} }

View File

@ -249,7 +249,6 @@
}, },
"user": { "user": {
"name": "Name", "name": "Name",
"icon": "Symbol",
"selectAnIcon": "Wählen Sie ein Symbol aus", "selectAnIcon": "Wählen Sie ein Symbol aus",
"pleaseInputYourOpenAIKey": "Bitte geben Sie Ihren OpenAI-Schlüssel ein" "pleaseInputYourOpenAIKey": "Bitte geben Sie Ihren OpenAI-Schlüssel ein"
} }

View File

@ -331,7 +331,7 @@
"user": { "user": {
"name": "Name", "name": "Name",
"email": "Email", "email": "Email",
"icon": "Icon", "tooltipSelectIcon": "Select icon",
"selectAnIcon": "Select an icon", "selectAnIcon": "Select an icon",
"pleaseInputYourOpenAIKey": "please input your OpenAI key", "pleaseInputYourOpenAIKey": "please input your OpenAI key",
"clickToLogout": "Click to logout the current user" "clickToLogout": "Click to logout the current user"

View File

@ -246,7 +246,6 @@
}, },
"user": { "user": {
"name": "Nombre", "name": "Nombre",
"icon": "Icono",
"selectAnIcon": "Seleccione un icono", "selectAnIcon": "Seleccione un icono",
"pleaseInputYourOpenAIKey": "por favor ingrese su clave OpenAI" "pleaseInputYourOpenAIKey": "por favor ingrese su clave OpenAI"
} }

View File

@ -258,7 +258,6 @@
}, },
"user": { "user": {
"name": "Izena", "name": "Izena",
"icon": "Ikonoa",
"selectAnIcon": "Hautatu ikono bat", "selectAnIcon": "Hautatu ikono bat",
"pleaseInputYourOpenAIKey": "mesedez sartu zure OpenAI gakoa" "pleaseInputYourOpenAIKey": "mesedez sartu zure OpenAI gakoa"
} }

View File

@ -296,7 +296,6 @@
}, },
"user": { "user": {
"name": "نام", "name": "نام",
"icon": "آیکون",
"selectAnIcon": "انتخاب یک آیکون", "selectAnIcon": "انتخاب یک آیکون",
"pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید", "pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید",
"clickToLogout": "برای خروج از کاربر فعلی کلیک کنید" "clickToLogout": "برای خروج از کاربر فعلی کلیک کنید"

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "Nom", "name": "Nom",
"icon": "Icône",
"selectAnIcon": "Sélectionnez une icône", "selectAnIcon": "Sélectionnez une icône",
"pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI" "pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI"
} }

View File

@ -250,7 +250,6 @@
}, },
"user": { "user": {
"name": "Nom", "name": "Nom",
"icon": "Icône",
"selectAnIcon": "Sélectionnez une icône", "selectAnIcon": "Sélectionnez une icône",
"pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI" "pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI"
} }

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "Név", "name": "Név",
"icon": "Ikon",
"selectAnIcon": "Válasszon ki egy ikont", "selectAnIcon": "Válasszon ki egy ikont",
"pleaseInputYourOpenAIKey": "kérjük, adja meg OpenAI kulcsát" "pleaseInputYourOpenAIKey": "kérjük, adja meg OpenAI kulcsát"
} }

View File

@ -246,7 +246,6 @@
}, },
"user": { "user": {
"name": "Nama", "name": "Nama",
"icon": "Ikon",
"selectAnIcon": "Pilih ikon", "selectAnIcon": "Pilih ikon",
"pleaseInputYourOpenAIKey": "silakan masukkan kunci OpenAI Anda" "pleaseInputYourOpenAIKey": "silakan masukkan kunci OpenAI Anda"
} }

View File

@ -241,7 +241,6 @@
}, },
"user": { "user": {
"name": "Nome", "name": "Nome",
"icon": "Icona",
"selectAnIcon": "Seleziona un'icona", "selectAnIcon": "Seleziona un'icona",
"pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI" "pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI"
}, },

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "名前", "name": "名前",
"icon": "アイコン",
"selectAnIcon": "アイコンを選択してください", "selectAnIcon": "アイコンを選択してください",
"pleaseInputYourOpenAIKey": "OpenAI キーを入力してください" "pleaseInputYourOpenAIKey": "OpenAI キーを入力してください"
} }

View File

@ -252,7 +252,6 @@
}, },
"user": { "user": {
"name": "이름", "name": "이름",
"icon": "상",
"selectAnIcon": "아이콘을 선택하세요", "selectAnIcon": "아이콘을 선택하세요",
"pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오" "pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오"
} }

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "Nazwa", "name": "Nazwa",
"icon": "Ikona",
"selectAnIcon": "Wybierz ikonę", "selectAnIcon": "Wybierz ikonę",
"pleaseInputYourOpenAIKey": "wprowadź swój klucz OpenAI" "pleaseInputYourOpenAIKey": "wprowadź swój klucz OpenAI"
} }

View File

@ -291,7 +291,6 @@
}, },
"user": { "user": {
"name": "Nome", "name": "Nome",
"icon": "Ícone",
"selectAnIcon": "Escolha um ícone", "selectAnIcon": "Escolha um ícone",
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI", "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
"email": "E-mail", "email": "E-mail",

View File

@ -269,7 +269,6 @@
}, },
"user": { "user": {
"name": "Nome", "name": "Nome",
"icon": "Ícone",
"selectAnIcon": "Selecione um ícone", "selectAnIcon": "Selecione um ícone",
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI", "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
"email": "E-mail", "email": "E-mail",

View File

@ -265,7 +265,6 @@
}, },
"user": { "user": {
"name": "Имя", "name": "Имя",
"icon": "Иконка",
"selectAnIcon": "Выбрать иконку", "selectAnIcon": "Выбрать иконку",
"pleaseInputYourOpenAIKey": "Введите токен OpenAI" "pleaseInputYourOpenAIKey": "Введите токен OpenAI"
} }

View File

@ -250,7 +250,6 @@
}, },
"user": { "user": {
"name": "namn", "name": "namn",
"icon": "Ikon",
"selectAnIcon": "Välj en ikon", "selectAnIcon": "Välj en ikon",
"pleaseInputYourOpenAIKey": "vänligen ange din OpenAI-nyckel" "pleaseInputYourOpenAIKey": "vänligen ange din OpenAI-nyckel"
} }

View File

@ -240,7 +240,6 @@
}, },
"user": { "user": {
"name": "İsim", "name": "İsim",
"icon": "Simge",
"selectAnIcon": "Bir simge seçin", "selectAnIcon": "Bir simge seçin",
"pleaseInputYourOpenAIKey": "lütfen OpenAI anahtarınızı girin" "pleaseInputYourOpenAIKey": "lütfen OpenAI anahtarınızı girin"
} }

View File

@ -331,7 +331,6 @@
"user": { "user": {
"name": "نام", "name": "نام",
"email": "ای میل", "email": "ای میل",
"icon": "آئیکن",
"selectAnIcon": "آئیکن منتخب کریں", "selectAnIcon": "آئیکن منتخب کریں",
"pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں", "pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں",
"clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں" "clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں"

View File

@ -267,7 +267,6 @@
}, },
"user": { "user": {
"name": "名字", "name": "名字",
"icon": "图标",
"selectAnIcon": "选择一个图标", "selectAnIcon": "选择一个图标",
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥" "pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
} }

View File

@ -258,7 +258,6 @@
}, },
"user": { "user": {
"name": "名稱", "name": "名稱",
"icon": "圖標",
"selectAnIcon": "選擇圖標", "selectAnIcon": "選擇圖標",
"pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰" "pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰"
} }