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:
21pages
2026-03-03 21:38:46 +08:00
parent 890282e385
commit d9e2107fb8
10 changed files with 90 additions and 108 deletions

View File

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

View File

@@ -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,