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:
@ -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) {
|
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(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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.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 {
|
||||||
|
@ -258,7 +258,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "اسم",
|
"name": "اسم",
|
||||||
"icon": "أيقونة",
|
|
||||||
"selectAnIcon": "حدد أيقونة",
|
"selectAnIcon": "حدد أيقونة",
|
||||||
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
|
"pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
"icon": "آیکون",
|
|
||||||
"selectAnIcon": "انتخاب یک آیکون",
|
"selectAnIcon": "انتخاب یک آیکون",
|
||||||
"pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید",
|
"pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید",
|
||||||
"clickToLogout": "برای خروج از کاربر فعلی کلیک کنید"
|
"clickToLogout": "برای خروج از کاربر فعلی کلیک کنید"
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -240,7 +240,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "名前",
|
"name": "名前",
|
||||||
"icon": "アイコン",
|
|
||||||
"selectAnIcon": "アイコンを選択してください",
|
"selectAnIcon": "アイコンを選択してください",
|
||||||
"pleaseInputYourOpenAIKey": "OpenAI キーを入力してください"
|
"pleaseInputYourOpenAIKey": "OpenAI キーを入力してください"
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "이름",
|
"name": "이름",
|
||||||
"icon": "상",
|
|
||||||
"selectAnIcon": "아이콘을 선택하세요",
|
"selectAnIcon": "아이콘을 선택하세요",
|
||||||
"pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오"
|
"pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -265,7 +265,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "Имя",
|
"name": "Имя",
|
||||||
"icon": "Иконка",
|
|
||||||
"selectAnIcon": "Выбрать иконку",
|
"selectAnIcon": "Выбрать иконку",
|
||||||
"pleaseInputYourOpenAIKey": "Введите токен OpenAI"
|
"pleaseInputYourOpenAIKey": "Введите токен OpenAI"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,6 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"name": "نام",
|
"name": "نام",
|
||||||
"email": "ای میل",
|
"email": "ای میل",
|
||||||
"icon": "آئیکن",
|
|
||||||
"selectAnIcon": "آئیکن منتخب کریں",
|
"selectAnIcon": "آئیکن منتخب کریں",
|
||||||
"pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں",
|
"pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں",
|
||||||
"clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں"
|
"clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں"
|
||||||
|
@ -267,7 +267,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "名字",
|
"name": "名字",
|
||||||
"icon": "图标",
|
|
||||||
"selectAnIcon": "选择一个图标",
|
"selectAnIcon": "选择一个图标",
|
||||||
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
|
"pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥"
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,6 @@
|
|||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"name": "名稱",
|
"name": "名稱",
|
||||||
"icon": "圖標",
|
|
||||||
"selectAnIcon": "選擇圖標",
|
"selectAnIcon": "選擇圖標",
|
||||||
"pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰"
|
"pleaseInputYourOpenAIKey": "請輸入你的 OpenAI 密鑰"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user