mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
047f1a0b39
commit
1b966171c4
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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) {
|
||||
_userService.updateUserProfile(openAIKey: openAIKey).then((result) {
|
||||
result.fold(
|
||||
@ -105,8 +114,9 @@ class SettingsUserEvent with _$SettingsUserEvent {
|
||||
const factory SettingsUserEvent.initial() = _Initial;
|
||||
const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName;
|
||||
const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail;
|
||||
const factory SettingsUserEvent.updateUserIcon(String iconUrl) =
|
||||
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
|
||||
_UpdateUserIcon;
|
||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
|
||||
_UpdateUserOpenaiKey;
|
||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||
|
@ -1,11 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.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/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
@ -34,7 +32,10 @@ class SidebarUser extends StatelessWidget {
|
||||
builder: (context, state) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildAvatar(context, state),
|
||||
UserAvatar(
|
||||
iconUrl: state.userProfile.iconUrl,
|
||||
name: state.userProfile.name,
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
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) {
|
||||
final String name = _userName(state.userProfile);
|
||||
return FlowyText.medium(
|
||||
|
@ -9,10 +9,14 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/util/debounce.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/flowy_tooltip.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.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_bloc/flutter_bloc.dart';
|
||||
|
||||
@ -49,21 +53,16 @@ class SettingsUserView extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_renderUserNameInput(context),
|
||||
|
||||
_buildUserIconSetting(context),
|
||||
if (isSupabaseEnabled) ...[
|
||||
const VSpace(20),
|
||||
const VSpace(12),
|
||||
UserEmailInput(user.email)
|
||||
],
|
||||
|
||||
const VSpace(20),
|
||||
_renderCurrentIcon(context),
|
||||
const VSpace(20),
|
||||
const VSpace(12),
|
||||
_renderCurrentOpenaiKey(context),
|
||||
const VSpace(20),
|
||||
// _renderHistoricalUser(context),
|
||||
const VSpace(12),
|
||||
_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.
|
||||
///
|
||||
/// 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 (state.userProfile.authType == AuthTypePB.Local) {
|
||||
return SettingThirdPartyLogin(
|
||||
didLogin: didLogin,
|
||||
);
|
||||
return SettingThirdPartyLogin(didLogin: didLogin);
|
||||
}
|
||||
|
||||
return SettingLogoutButton(user: user, didLogout: didLogout);
|
||||
@ -100,20 +206,49 @@ class SettingsUserView extends StatelessWidget {
|
||||
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) {
|
||||
final String openAIKey =
|
||||
context.read<SettingsUserViewBloc>().state.userProfile.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
|
||||
@ -317,69 +452,19 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
}
|
||||
}
|
||||
|
||||
class _CurrentIcon extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
typedef SelectIconCallback = void Function(String iconUrl, bool isSelected);
|
||||
|
||||
class IconGallery extends StatelessWidget {
|
||||
final Function setIcon;
|
||||
const IconGallery(this.setIcon, {Key? key}) : super(key: key);
|
||||
final String selectedIcon;
|
||||
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 {
|
||||
final manifestContent =
|
||||
@ -403,23 +488,29 @@ class IconGallery extends StatelessWidget {
|
||||
return FutureBuilder<List<String>>(
|
||||
future: _getIcons(context),
|
||||
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||
return GridView.count(
|
||||
padding: const EdgeInsets.all(20),
|
||||
crossAxisCount: 5,
|
||||
children: (snapshot.data ?? []).map((String iconUrl) {
|
||||
return IconOption(
|
||||
FlowySvgData('emoji/$iconUrl'),
|
||||
iconUrl,
|
||||
setIcon,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
children: [
|
||||
if (defaultOption != null) defaultOption!,
|
||||
...snapshot.data!
|
||||
.mapIndexed(
|
||||
(int index, String iconUrl) => IconOption(
|
||||
emoji: FlowySvgData('emoji/$iconUrl'),
|
||||
iconUrl: iconUrl,
|
||||
onSelectIcon: onSelectIcon,
|
||||
isSelected: iconUrl == selectedIcon,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -428,21 +519,34 @@ class IconGallery extends StatelessWidget {
|
||||
class IconOption extends StatelessWidget {
|
||||
final FlowySvgData emoji;
|
||||
final String iconUrl;
|
||||
final Function setIcon;
|
||||
final SelectIconCallback onSelectIcon;
|
||||
final bool isSelected;
|
||||
|
||||
IconOption(this.emoji, this.iconUrl, this.setIcon, {Key? key})
|
||||
: super(key: ValueKey(emoji));
|
||||
IconOption({
|
||||
required this.emoji,
|
||||
required this.iconUrl,
|
||||
required this.onSelectIcon,
|
||||
required this.isSelected,
|
||||
}) : super(key: ValueKey(emoji));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: Corners.s6Border,
|
||||
borderRadius: Corners.s8Border,
|
||||
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
onTap: () => setIcon(iconUrl),
|
||||
child: FlowySvg(
|
||||
emoji,
|
||||
size: _iconSize,
|
||||
blendMode: null,
|
||||
onTap: () => onSelectIcon(iconUrl, isSelected),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
child: FlowySvg(
|
||||
emoji,
|
||||
size: _iconSize,
|
||||
blendMode: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -118,6 +118,15 @@ class HoverStyle {
|
||||
this.hoverColor,
|
||||
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 {
|
||||
|
@ -258,7 +258,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "اسم",
|
||||
"icon": "أيقونة",
|
||||
"selectAnIcon": "حدد أيقونة",
|
||||
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
|
||||
}
|
||||
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nom",
|
||||
"icon": "Icona",
|
||||
"selectAnIcon": "Seleccioneu una icona",
|
||||
"pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau OpenAI"
|
||||
}
|
||||
|
@ -249,7 +249,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Name",
|
||||
"icon": "Symbol",
|
||||
"selectAnIcon": "Wählen Sie ein Symbol aus",
|
||||
"pleaseInputYourOpenAIKey": "Bitte geben Sie Ihren OpenAI-Schlüssel ein"
|
||||
}
|
||||
|
@ -331,7 +331,7 @@
|
||||
"user": {
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"icon": "Icon",
|
||||
"tooltipSelectIcon": "Select icon",
|
||||
"selectAnIcon": "Select an icon",
|
||||
"pleaseInputYourOpenAIKey": "please input your OpenAI key",
|
||||
"clickToLogout": "Click to logout the current user"
|
||||
|
@ -246,7 +246,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nombre",
|
||||
"icon": "Icono",
|
||||
"selectAnIcon": "Seleccione un icono",
|
||||
"pleaseInputYourOpenAIKey": "por favor ingrese su clave OpenAI"
|
||||
}
|
||||
|
@ -258,7 +258,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Izena",
|
||||
"icon": "Ikonoa",
|
||||
"selectAnIcon": "Hautatu ikono bat",
|
||||
"pleaseInputYourOpenAIKey": "mesedez sartu zure OpenAI gakoa"
|
||||
}
|
||||
|
@ -296,7 +296,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "نام",
|
||||
"icon": "آیکون",
|
||||
"selectAnIcon": "انتخاب یک آیکون",
|
||||
"pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید",
|
||||
"clickToLogout": "برای خروج از کاربر فعلی کلیک کنید"
|
||||
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nom",
|
||||
"icon": "Icône",
|
||||
"selectAnIcon": "Sélectionnez une icône",
|
||||
"pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI"
|
||||
}
|
||||
|
@ -250,7 +250,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nom",
|
||||
"icon": "Icône",
|
||||
"selectAnIcon": "Sélectionnez une icône",
|
||||
"pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI"
|
||||
}
|
||||
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Név",
|
||||
"icon": "Ikon",
|
||||
"selectAnIcon": "Válasszon ki egy ikont",
|
||||
"pleaseInputYourOpenAIKey": "kérjük, adja meg OpenAI kulcsát"
|
||||
}
|
||||
|
@ -246,7 +246,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nama",
|
||||
"icon": "Ikon",
|
||||
"selectAnIcon": "Pilih ikon",
|
||||
"pleaseInputYourOpenAIKey": "silakan masukkan kunci OpenAI Anda"
|
||||
}
|
||||
|
@ -241,7 +241,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nome",
|
||||
"icon": "Icona",
|
||||
"selectAnIcon": "Seleziona un'icona",
|
||||
"pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI"
|
||||
},
|
||||
@ -608,4 +607,4 @@
|
||||
"deleteContentTitle": "Sei sicuro di voler eliminare {pageType}?",
|
||||
"deleteContentCaption": "se elimini questo {pageType}, puoi ripristinarlo dal cestino."
|
||||
}
|
||||
}
|
||||
}
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "名前",
|
||||
"icon": "アイコン",
|
||||
"selectAnIcon": "アイコンを選択してください",
|
||||
"pleaseInputYourOpenAIKey": "OpenAI キーを入力してください"
|
||||
}
|
||||
|
@ -252,7 +252,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "이름",
|
||||
"icon": "상",
|
||||
"selectAnIcon": "아이콘을 선택하세요",
|
||||
"pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오"
|
||||
}
|
||||
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nazwa",
|
||||
"icon": "Ikona",
|
||||
"selectAnIcon": "Wybierz ikonę",
|
||||
"pleaseInputYourOpenAIKey": "wprowadź swój klucz OpenAI"
|
||||
}
|
||||
|
@ -291,7 +291,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nome",
|
||||
"icon": "Ícone",
|
||||
"selectAnIcon": "Escolha um ícone",
|
||||
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
|
||||
"email": "E-mail",
|
||||
|
@ -269,7 +269,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Nome",
|
||||
"icon": "Ícone",
|
||||
"selectAnIcon": "Selecione um ícone",
|
||||
"pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI",
|
||||
"email": "E-mail",
|
||||
|
@ -265,7 +265,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "Имя",
|
||||
"icon": "Иконка",
|
||||
"selectAnIcon": "Выбрать иконку",
|
||||
"pleaseInputYourOpenAIKey": "Введите токен OpenAI"
|
||||
}
|
||||
|
@ -250,7 +250,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "namn",
|
||||
"icon": "Ikon",
|
||||
"selectAnIcon": "Välj en ikon",
|
||||
"pleaseInputYourOpenAIKey": "vänligen ange din OpenAI-nyckel"
|
||||
}
|
||||
@ -597,4 +596,4 @@
|
||||
"deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?",
|
||||
"deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen."
|
||||
}
|
||||
}
|
||||
}
|
@ -240,7 +240,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "İsim",
|
||||
"icon": "Simge",
|
||||
"selectAnIcon": "Bir simge seçin",
|
||||
"pleaseInputYourOpenAIKey": "lütfen OpenAI anahtarınızı girin"
|
||||
}
|
||||
|
@ -331,7 +331,6 @@
|
||||
"user": {
|
||||
"name": "نام",
|
||||
"email": "ای میل",
|
||||
"icon": "آئیکن",
|
||||
"selectAnIcon": "آئیکن منتخب کریں",
|
||||
"pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں",
|
||||
"clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں"
|
||||
|
@ -267,7 +267,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "名字",
|
||||
"icon": "图标",
|
||||
"selectAnIcon": "选择一个图标",
|
||||
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
|
||||
}
|
||||
|
@ -258,7 +258,6 @@
|
||||
},
|
||||
"user": {
|
||||
"name": "名稱",
|
||||
"icon": "圖標",
|
||||
"selectAnIcon": "選擇圖標",
|
||||
"pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user