refactor: Implement avatar image viewer and adjust design

Signed-off-by: Krille <c.kussowski@famedly.com>
This commit is contained in:
Krille 2025-04-13 11:04:52 +02:00
parent 2873a047f8
commit 3594fa4f6d
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
12 changed files with 151 additions and 59 deletions

View file

@ -18,6 +18,7 @@ import '../key_verification/key_verification_dialog.dart';
class BootstrapDialog extends StatefulWidget { class BootstrapDialog extends StatefulWidget {
final bool wipe; final bool wipe;
final Client client; final Client client;
const BootstrapDialog({ const BootstrapDialog({
super.key, super.key,
this.wipe = false, this.wipe = false,
@ -132,7 +133,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
minLines: 2, minLines: 2,
maxLines: 4, maxLines: 4,
readOnly: true, readOnly: true,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
controller: TextEditingController(text: key), controller: TextEditingController(text: key),
decoration: const InputDecoration( decoration: const InputDecoration(
contentPadding: EdgeInsets.all(16), contentPadding: EdgeInsets.all(16),
@ -257,7 +258,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
? null ? null
: [AutofillHints.password], : [AutofillHints.password],
controller: _recoveryKeyTextEditingController, controller: _recoveryKeyTextEditingController,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle( hintStyle: TextStyle(

View file

@ -295,7 +295,7 @@ class HtmlMessage extends StatelessWidget {
), ),
textStyle: TextStyle( textStyle: TextStyle(
fontSize: fontSize, fontSize: fontSize,
fontFamily: 'UbuntuMono', fontFamily: 'RobotoMono',
), ),
), ),
), ),

View file

@ -235,7 +235,7 @@ class InputBar extends StatelessWidget {
children: [ children: [
Text( Text(
commandExample(command), commandExample(command),
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
), ),
Text( Text(
hint, hint,
@ -255,7 +255,7 @@ class InputBar extends StatelessWidget {
waitDuration: const Duration(days: 1), // don't show on hover waitDuration: const Duration(days: 1), // don't show on hover
child: Container( child: Container(
padding: padding, padding: padding,
child: Text(label, style: const TextStyle(fontFamily: 'UbuntuMono')), child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
), ),
); );
} }

View file

@ -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/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
import '../../widgets/mxc_image_viewer.dart';
import '../../widgets/qr_code_viewer.dart'; import '../../widgets/qr_code_viewer.dart';
class ChatDetailsView extends StatelessWidget { class ChatDetailsView extends StatelessWidget {
@ -38,6 +39,7 @@ class ChatDetailsView extends StatelessWidget {
} }
final directChatMatrixID = room.directChatMatrixID; final directChatMatrixID = room.directChatMatrixID;
final roomAvatar = room.avatar;
return StreamBuilder( return StreamBuilder(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream
@ -108,6 +110,13 @@ class ChatDetailsView extends StatelessWidget {
mxContent: room.avatar, mxContent: room.avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: roomAvatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(roomAvatar),
)
: null,
), ),
), ),
if (!room.isDirectChat && if (!room.isDirectChat &&

View file

@ -169,7 +169,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].ed25519Key?.beautified ?? deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context).unknownEncryptionAlgorithm, L10n.of(context).unknownEncryptionAlgorithm,
style: TextStyle( style: TextStyle(
fontFamily: 'UbuntuMono', fontFamily: 'RobotoMono',
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
), ),
), ),

View file

@ -12,6 +12,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/navigation_rail.dart'; import 'package:fluffychat/widgets/navigation_rail.dart';
import '../../widgets/mxc_image_viewer.dart';
import 'settings.dart'; import 'settings.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
@ -65,6 +66,7 @@ class SettingsView extends StatelessWidget {
future: controller.profileFuture, future: controller.profileFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
final profile = snapshot.data; final profile = snapshot.data;
final avatar = profile?.avatarUrl;
final mxid = Matrix.of(context).client.userID ?? final mxid = Matrix.of(context).client.userID ??
L10n.of(context).user; L10n.of(context).user;
final displayname = final displayname =
@ -76,9 +78,16 @@ class SettingsView extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
Avatar( Avatar(
mxContent: profile?.avatarUrl, mxContent: avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(avatar),
)
: null,
), ),
if (profile != null) if (profile != null)
Positioned( Positioned(

View file

@ -16,6 +16,7 @@ import 'settings_security.dart';
class SettingsSecurityView extends StatelessWidget { class SettingsSecurityView extends StatelessWidget {
final SettingsSecurityController controller; final SettingsSecurityController controller;
const SettingsSecurityView(this.controller, {super.key}); const SettingsSecurityView(this.controller, {super.key});
@override @override
@ -143,7 +144,7 @@ class SettingsSecurityView extends StatelessWidget {
leading: const Icon(Icons.vpn_key_outlined), leading: const Icon(Icons.vpn_key_outlined),
subtitle: SelectableText( subtitle: SelectableText(
Matrix.of(context).client.fingerprintKey.beautified, Matrix.of(context).client.fingerprintKey.beautified,
style: const TextStyle(fontFamily: 'UbuntuMono'), style: const TextStyle(fontFamily: 'RobotoMono'),
), ),
), ),
if (capabilities?.mChangePassword?.enabled != false || if (capabilities?.mChangePassword?.enabled != false ||

View file

@ -9,7 +9,7 @@ extension StringColor on String {
number += codeUnitAt(i); number += codeUnitAt(i);
} }
number = (number % 12) * 25.5; 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 { Color get color {
@ -29,6 +29,6 @@ extension StringColor on String {
Color get lightColorAvatar { Color get lightColorAvatar {
_colorCache[this] ??= {}; _colorCache[this] ??= {};
return _colorCache[this]![0.4] ??= _getColorLight(0.4); return _colorCache[this]![0.45] ??= _getColorLight(0.45);
} }
} }

View file

@ -15,6 +15,7 @@ import '../../utils/url_launcher.dart';
import '../future_loading_dialog.dart'; import '../future_loading_dialog.dart';
import '../hover_builder.dart'; import '../hover_builder.dart';
import '../matrix.dart'; import '../matrix.dart';
import '../mxc_image_viewer.dart';
class UserDialog extends StatelessWidget { class UserDialog extends StatelessWidget {
static Future<void> show({ static Future<void> show({
@ -45,6 +46,7 @@ class UserDialog extends StatelessWidget {
L10n.of(context).user; L10n.of(context).user;
var copied = false; var copied = false;
final theme = Theme.of(context); final theme = Theme.of(context);
final avatar = profile.avatarUrl;
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: ConstrainedBox( title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256), constraints: const BoxConstraints(maxWidth: 256),
@ -75,7 +77,9 @@ class UserDialog extends StatelessWidget {
children: [ children: [
HoverBuilder( HoverBuilder(
builder: (context, hovered) => StatefulBuilder( builder: (context, hovered) => StatefulBuilder(
builder: (context, setState) => GestureDetector( builder: (context, setState) => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () { onTap: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: profile.userId), ClipboardData(text: profile.userId),
@ -89,9 +93,11 @@ class UserDialog extends StatelessWidget {
children: [ children: [
WidgetSpan( WidgetSpan(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding:
const EdgeInsets.only(right: 4.0),
child: AnimatedScale( child: AnimatedScale(
duration: FluffyThemes.animationDuration, duration:
FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
scale: hovered scale: hovered
? 1.33 ? 1.33
@ -118,11 +124,18 @@ class UserDialog extends StatelessWidget {
), ),
), ),
), ),
),
Center( Center(
child: Avatar( child: Avatar(
mxContent: profile.avatarUrl, mxContent: avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2, size: Avatar.defaultSize * 2,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) => MxcImageViewer(avatar),
)
: null,
), ),
), ),
if (presenceText != null) if (presenceText != null)

View file

@ -62,18 +62,13 @@ class Avatar extends StatelessWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: noPic child: noPic
? Container( ? Container(
decoration: BoxDecoration( decoration: BoxDecoration(color: name!.lightColorAvatar),
gradient: LinearGradient(
colors: [name!.lightColorAvatar, name.color],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
fallbackLetters, fallbackLetters,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: 'RobotoMono',
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: (size / 2.5).roundToDouble(), fontSize: (size / 2.5).roundToDouble(),
@ -143,10 +138,12 @@ class Avatar extends StatelessWidget {
], ],
); );
if (onTap == null) return container; if (onTap == null) return container;
return InkWell( return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onTap, onTap: onTap,
borderRadius: borderRadius,
child: container, child: container,
),
); );
} }
} }

View file

@ -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,
),
),
),
),
),
);
}
}

View file

@ -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/adaptive_dialog_action.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.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<int?> showPermissionChooser( Future<int?> showPermissionChooser(
BuildContext context, { BuildContext context, {