mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-21 12:11:06 +03:00
refactor avatar display: unify rendering and resolve at use time
- Extract buildAvatarWidget() in common.dart to share avatar rendering
logic across desktop settings, desktop CM and mobile CM
- Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
- Store avatar as-is in local config, only resolve when displaying
(settings page) or sending (LoginRequest)
- Resolve avatar in LoginRequest before sending to remote peer
- Add error handling for network image load failures
- Guard against empty client.name[0] crash
- Show avatar in mobile settings page account tile
Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
@@ -4118,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an avatar widget from an avatar URL or data URI string.
|
||||
/// Returns [fallback] if avatar is empty or cannot be decoded.
|
||||
/// [borderRadius] defaults to [size]/2 (circle).
|
||||
Widget? buildAvatarWidget({
|
||||
required String avatar,
|
||||
required double size,
|
||||
double? borderRadius,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final trimmed = avatar.trim();
|
||||
if (trimmed.isEmpty) return fallback;
|
||||
|
||||
ImageProvider? imageProvider;
|
||||
if (trimmed.startsWith('data:image/')) {
|
||||
final comma = trimmed.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
|
||||
} catch (_) {}
|
||||
}
|
||||
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
imageProvider = NetworkImage(trimmed);
|
||||
}
|
||||
|
||||
if (imageProvider == null) return fallback;
|
||||
|
||||
final radius = borderRadius ?? size / 2;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
fallback ?? SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2061,7 +2061,8 @@ class _AccountState extends State<_Account> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -2076,28 +2077,13 @@ class _AccountState extends State<_Account> {
|
||||
}
|
||||
|
||||
Widget? _buildUserAvatar() {
|
||||
final avatar = gFFI.userModel.avatar.value.trim();
|
||||
if (avatar.isEmpty) return null;
|
||||
const radius = 22.0;
|
||||
if (avatar.startsWith('data:image/')) {
|
||||
final comma = avatar.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundImage: MemoryImage(base64Decode(avatar.substring(comma + 1))),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundImage: NetworkImage(avatar),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
// Resolve relative avatar path at display time
|
||||
final avatar =
|
||||
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 44,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// original cm window in Sciter version.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -569,37 +568,12 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
Widget _buildClientAvatar() {
|
||||
const borderRadius = BorderRadius.all(Radius.circular(15.0));
|
||||
final avatar = client.avatar.trim();
|
||||
if (avatar.startsWith('data:image/')) {
|
||||
final comma = avatar.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
final bytes = base64Decode(avatar.substring(comma + 1));
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
|
||||
return ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: Image.network(
|
||||
avatar,
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _buildInitialAvatar(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildInitialAvatar();
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 70,
|
||||
borderRadius: 15,
|
||||
fallback: _buildInitialAvatar(),
|
||||
)!;
|
||||
}
|
||||
|
||||
Widget _buildInitialAvatar() {
|
||||
@@ -612,7 +586,7 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Text(
|
||||
client.name[0],
|
||||
client.name.isNotEmpty ? client.name[0] : '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -857,28 +856,17 @@ class ClientInfo extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildAvatar(BuildContext context) {
|
||||
final avatar = client.avatar.trim();
|
||||
if (avatar.isNotEmpty) {
|
||||
if (avatar.startsWith('data:image/')) {
|
||||
final comma = avatar.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
return CircleAvatar(
|
||||
backgroundImage: MemoryImage(base64Decode(avatar.substring(comma + 1))),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
} else if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
|
||||
return CircleAvatar(backgroundImage: NetworkImage(avatar));
|
||||
}
|
||||
}
|
||||
// Show character as before if no avatar
|
||||
return CircleAvatar(
|
||||
final fallback = CircleAvatar(
|
||||
backgroundColor: str2color(
|
||||
client.name,
|
||||
Theme.of(context).brightness == Brightness.light ? 255 : 150),
|
||||
child: Text(client.name[0]),
|
||||
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
|
||||
);
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 40,
|
||||
fallback: fallback,
|
||||
)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -689,7 +689,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
||||
leading: Icon(Icons.person),
|
||||
leading: Obx(() {
|
||||
final avatar = bind.mainResolveAvatarUrl(
|
||||
avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 40,
|
||||
) ??
|
||||
Icon(Icons.person);
|
||||
}),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
|
||||
@@ -34,6 +34,7 @@ class UserModel {
|
||||
}
|
||||
return '$preferred (@$username)';
|
||||
}
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
UserModel(this.parent) {
|
||||
@@ -88,7 +89,6 @@ class UserModel {
|
||||
}
|
||||
|
||||
final user = UserPayload.fromJson(data);
|
||||
user.avatar = _resolveAvatar(user.avatar, url);
|
||||
_parseAndUpdateUser(user);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to refreshCurrentUser: $e');
|
||||
@@ -138,7 +138,6 @@ class UserModel {
|
||||
avatar.value = user.avatar;
|
||||
isAdmin.value = user.isAdmin;
|
||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||
_updateLocalUserInfo();
|
||||
if (isWeb) {
|
||||
// ugly here, tmp solution
|
||||
bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');
|
||||
|
||||
Reference in New Issue
Block a user