From 3594fa4f6dc640e34a9cb4c226a8e48409b69020 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 13 Apr 2025 11:04:52 +0200 Subject: [PATCH] refactor: Implement avatar image viewer and adjust design Signed-off-by: Krille --- lib/pages/bootstrap/bootstrap_dialog.dart | 5 +- lib/pages/chat/events/html_message.dart | 2 +- lib/pages/chat/input_bar.dart | 4 +- lib/pages/chat_details/chat_details_view.dart | 9 ++ .../chat_encryption_settings_view.dart | 2 +- lib/pages/settings/settings_view.dart | 11 ++- .../settings_security_view.dart | 3 +- lib/utils/string_color.dart | 4 +- lib/widgets/adaptive_dialogs/user_dialog.dart | 85 +++++++++++-------- lib/widgets/avatar.dart | 19 ++--- lib/widgets/mxc_image_viewer.dart | 60 +++++++++++++ lib/widgets/permission_slider_dialog.dart | 6 +- 12 files changed, 151 insertions(+), 59 deletions(-) create mode 100644 lib/widgets/mxc_image_viewer.dart diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index c2f35fc78..61441eb6d 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -18,6 +18,7 @@ import '../key_verification/key_verification_dialog.dart'; class BootstrapDialog extends StatefulWidget { final bool wipe; final Client client; + const BootstrapDialog({ super.key, this.wipe = false, @@ -132,7 +133,7 @@ class BootstrapDialogState extends State { minLines: 2, maxLines: 4, readOnly: true, - style: const TextStyle(fontFamily: 'UbuntuMono'), + style: const TextStyle(fontFamily: 'RobotoMono'), controller: TextEditingController(text: key), decoration: const InputDecoration( contentPadding: EdgeInsets.all(16), @@ -257,7 +258,7 @@ class BootstrapDialogState extends State { ? null : [AutofillHints.password], controller: _recoveryKeyTextEditingController, - style: const TextStyle(fontFamily: 'UbuntuMono'), + style: const TextStyle(fontFamily: 'RobotoMono'), decoration: InputDecoration( contentPadding: const EdgeInsets.all(16), hintStyle: TextStyle( diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index f093d1090..d40ac72a7 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -295,7 +295,7 @@ class HtmlMessage extends StatelessWidget { ), textStyle: TextStyle( fontSize: fontSize, - fontFamily: 'UbuntuMono', + fontFamily: 'RobotoMono', ), ), ), diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 42b88b0b7..7be7a3c7d 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -235,7 +235,7 @@ class InputBar extends StatelessWidget { children: [ Text( commandExample(command), - style: const TextStyle(fontFamily: 'UbuntuMono'), + style: const TextStyle(fontFamily: 'RobotoMono'), ), Text( hint, @@ -255,7 +255,7 @@ class InputBar extends StatelessWidget { waitDuration: const Duration(days: 1), // don't show on hover child: Container( padding: padding, - child: Text(label, style: const TextStyle(fontFamily: 'UbuntuMono')), + child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')), ), ); } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index d368f0211..e96c10d82 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/url_launcher.dart'; +import '../../widgets/mxc_image_viewer.dart'; import '../../widgets/qr_code_viewer.dart'; class ChatDetailsView extends StatelessWidget { @@ -38,6 +39,7 @@ class ChatDetailsView extends StatelessWidget { } final directChatMatrixID = room.directChatMatrixID; + final roomAvatar = room.avatar; return StreamBuilder( stream: room.client.onRoomState.stream @@ -108,6 +110,13 @@ class ChatDetailsView extends StatelessWidget { mxContent: room.avatar, name: displayname, size: Avatar.defaultSize * 2.5, + onTap: roomAvatar != null + ? () => showDialog( + context: context, + builder: (_) => + MxcImageViewer(roomAvatar), + ) + : null, ), ), if (!room.isDirectChat && diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 4a61facbc..2c39a942e 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { deviceKeys[i].ed25519Key?.beautified ?? L10n.of(context).unknownEncryptionAlgorithm, style: TextStyle( - fontFamily: 'UbuntuMono', + fontFamily: 'RobotoMono', color: theme.colorScheme.secondary, ), ), diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index baa719550..eddbc76e8 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/navigation_rail.dart'; +import '../../widgets/mxc_image_viewer.dart'; import 'settings.dart'; class SettingsView extends StatelessWidget { @@ -65,6 +66,7 @@ class SettingsView extends StatelessWidget { future: controller.profileFuture, builder: (context, snapshot) { final profile = snapshot.data; + final avatar = profile?.avatarUrl; final mxid = Matrix.of(context).client.userID ?? L10n.of(context).user; final displayname = @@ -76,9 +78,16 @@ class SettingsView extends StatelessWidget { child: Stack( children: [ Avatar( - mxContent: profile?.avatarUrl, + mxContent: avatar, name: displayname, size: Avatar.defaultSize * 2.5, + onTap: avatar != null + ? () => showDialog( + context: context, + builder: (_) => + MxcImageViewer(avatar), + ) + : null, ), if (profile != null) Positioned( diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index 0861f1351..5d729091d 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -16,6 +16,7 @@ import 'settings_security.dart'; class SettingsSecurityView extends StatelessWidget { final SettingsSecurityController controller; + const SettingsSecurityView(this.controller, {super.key}); @override @@ -143,7 +144,7 @@ class SettingsSecurityView extends StatelessWidget { leading: const Icon(Icons.vpn_key_outlined), subtitle: SelectableText( Matrix.of(context).client.fingerprintKey.beautified, - style: const TextStyle(fontFamily: 'UbuntuMono'), + style: const TextStyle(fontFamily: 'RobotoMono'), ), ), if (capabilities?.mChangePassword?.enabled != false || diff --git a/lib/utils/string_color.dart b/lib/utils/string_color.dart index d854d1527..a0dfbf5c0 100644 --- a/lib/utils/string_color.dart +++ b/lib/utils/string_color.dart @@ -9,7 +9,7 @@ extension StringColor on String { number += codeUnitAt(i); } number = (number % 12) * 25.5; - return HSLColor.fromAHSL(1, number, 1, light).toColor(); + return HSLColor.fromAHSL(0.75, number, 1, light).toColor(); } Color get color { @@ -29,6 +29,6 @@ extension StringColor on String { Color get lightColorAvatar { _colorCache[this] ??= {}; - return _colorCache[this]![0.4] ??= _getColorLight(0.4); + return _colorCache[this]![0.45] ??= _getColorLight(0.45); } } diff --git a/lib/widgets/adaptive_dialogs/user_dialog.dart b/lib/widgets/adaptive_dialogs/user_dialog.dart index 5d17dd791..34fe1739f 100644 --- a/lib/widgets/adaptive_dialogs/user_dialog.dart +++ b/lib/widgets/adaptive_dialogs/user_dialog.dart @@ -15,6 +15,7 @@ import '../../utils/url_launcher.dart'; import '../future_loading_dialog.dart'; import '../hover_builder.dart'; import '../matrix.dart'; +import '../mxc_image_viewer.dart'; class UserDialog extends StatelessWidget { static Future show({ @@ -45,6 +46,7 @@ class UserDialog extends StatelessWidget { L10n.of(context).user; var copied = false; final theme = Theme.of(context); + final avatar = profile.avatarUrl; return AlertDialog.adaptive( title: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 256), @@ -75,54 +77,65 @@ class UserDialog extends StatelessWidget { children: [ HoverBuilder( builder: (context, hovered) => StatefulBuilder( - builder: (context, setState) => GestureDetector( - onTap: () { - Clipboard.setData( - ClipboardData(text: profile.userId), - ); - setState(() { - copied = true; - }); - }, - child: RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered - ? 1.33 - : copied - ? 1.25 - : 1.0, - child: Icon( - copied - ? Icons.check_circle - : Icons.copy, - size: 12, - color: copied ? Colors.green : null, + builder: (context, setState) => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + Clipboard.setData( + ClipboardData(text: profile.userId), + ); + setState(() { + copied = true; + }); + }, + child: RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: + const EdgeInsets.only(right: 4.0), + child: AnimatedScale( + duration: + FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered + ? 1.33 + : copied + ? 1.25 + : 1.0, + child: Icon( + copied + ? Icons.check_circle + : Icons.copy, + size: 12, + color: copied ? Colors.green : null, + ), ), ), ), - ), - TextSpan(text: profile.userId), - ], - style: theme.textTheme.bodyMedium - ?.copyWith(fontSize: 10), + TextSpan(text: profile.userId), + ], + style: theme.textTheme.bodyMedium + ?.copyWith(fontSize: 10), + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, ), ), ), ), Center( child: Avatar( - mxContent: profile.avatarUrl, + mxContent: avatar, name: displayname, size: Avatar.defaultSize * 2, + onTap: avatar != null + ? () => showDialog( + context: context, + builder: (_) => MxcImageViewer(avatar), + ) + : null, ), ), if (presenceText != null) diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 17668b2be..0a60c1b4a 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -62,18 +62,13 @@ class Avatar extends StatelessWidget { clipBehavior: Clip.hardEdge, child: noPic ? Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [name!.lightColorAvatar, name.color], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), + decoration: BoxDecoration(color: name!.lightColorAvatar), alignment: Alignment.center, child: Text( fallbackLetters, textAlign: TextAlign.center, style: TextStyle( + fontFamily: 'RobotoMono', color: Colors.white, fontWeight: FontWeight.bold, fontSize: (size / 2.5).roundToDouble(), @@ -143,10 +138,12 @@ class Avatar extends StatelessWidget { ], ); if (onTap == null) return container; - return InkWell( - onTap: onTap, - borderRadius: borderRadius, - child: container, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: container, + ), ); } } diff --git a/lib/widgets/mxc_image_viewer.dart b/lib/widgets/mxc_image_viewer.dart new file mode 100644 index 000000000..334f28224 --- /dev/null +++ b/lib/widgets/mxc_image_viewer.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'mxc_image.dart'; + +class MxcImageViewer extends StatelessWidget { + final Uri mxContent; + + const MxcImageViewer(this.mxContent, {super.key}); + + @override + Widget build(BuildContext context) { + final iconButtonStyle = IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(200), + foregroundColor: Colors.white, + ); + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.black.withAlpha(128), + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, + color: Colors.white, + tooltip: L10n.of(context).close, + ), + backgroundColor: Colors.transparent, + ), + body: InteractiveViewer( + minScale: 1.0, + maxScale: 10.0, + onInteractionEnd: (endDetails) { + if (endDetails.velocity.pixelsPerSecond.dy > + MediaQuery.of(context).size.height * 1.5) { + Navigator.of(context, rootNavigator: false).pop(); + } + }, + child: Center( + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: MxcImage( + key: ValueKey(mxContent.toString()), + uri: mxContent, + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/permission_slider_dialog.dart b/lib/widgets/permission_slider_dialog.dart index c31ee433f..fff6685e9 100644 --- a/lib/widgets/permission_slider_dialog.dart +++ b/lib/widgets/permission_slider_dialog.dart @@ -1,7 +1,9 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; Future showPermissionChooser( BuildContext context, {