mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-07 12:20:03 +03:00
Compare commits
3 Commits
890282e385
...
743705545a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743705545a | ||
|
|
89a34642a3 | ||
|
|
d9e2107fb8 |
@@ -4118,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) {
|
|||||||
return '';
|
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,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
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() {
|
Widget? _buildUserAvatar() {
|
||||||
final avatar = gFFI.userModel.avatar.value.trim();
|
// Resolve relative avatar path at display time
|
||||||
if (avatar.isEmpty) return null;
|
final avatar =
|
||||||
const radius = 22.0;
|
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
|
||||||
if (avatar.startsWith('data:image/')) {
|
return buildAvatarWidget(
|
||||||
final comma = avatar.indexOf(',');
|
avatar: avatar,
|
||||||
if (comma > 0) {
|
size: 44,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// original cm window in Sciter version.
|
// original cm window in Sciter version.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -569,37 +568,12 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
Widget _buildClientAvatar() {
|
Widget _buildClientAvatar() {
|
||||||
const borderRadius = BorderRadius.all(Radius.circular(15.0));
|
return buildAvatarWidget(
|
||||||
final avatar = client.avatar.trim();
|
avatar: client.avatar,
|
||||||
if (avatar.startsWith('data:image/')) {
|
size: 70,
|
||||||
final comma = avatar.indexOf(',');
|
borderRadius: 15,
|
||||||
if (comma > 0) {
|
fallback: _buildInitialAvatar(),
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInitialAvatar() {
|
Widget _buildInitialAvatar() {
|
||||||
@@ -612,7 +586,7 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(15.0),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
client.name[0],
|
client.name.isNotEmpty ? client.name[0] : '?',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -857,28 +856,17 @@ class ClientInfo extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAvatar(BuildContext context) {
|
Widget _buildAvatar(BuildContext context) {
|
||||||
final avatar = client.avatar.trim();
|
final fallback = CircleAvatar(
|
||||||
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(
|
|
||||||
backgroundColor: str2color(
|
backgroundColor: str2color(
|
||||||
client.name,
|
client.name,
|
||||||
Theme.of(context).brightness == Brightness.light ? 255 : 150),
|
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
|
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||||
? translate('Login')
|
? translate('Login')
|
||||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
: '${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) {
|
onPressed: (context) {
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
loginDialog();
|
loginDialog();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class UserModel {
|
|||||||
}
|
}
|
||||||
return '$preferred (@$username)';
|
return '$preferred (@$username)';
|
||||||
}
|
}
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
UserModel(this.parent) {
|
UserModel(this.parent) {
|
||||||
@@ -88,7 +89,6 @@ class UserModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final user = UserPayload.fromJson(data);
|
final user = UserPayload.fromJson(data);
|
||||||
user.avatar = _resolveAvatar(user.avatar, url);
|
|
||||||
_parseAndUpdateUser(user);
|
_parseAndUpdateUser(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to refreshCurrentUser: $e');
|
debugPrint('Failed to refreshCurrentUser: $e');
|
||||||
@@ -138,7 +138,6 @@ class UserModel {
|
|||||||
avatar.value = user.avatar;
|
avatar.value = user.avatar;
|
||||||
isAdmin.value = user.isAdmin;
|
isAdmin.value = user.isAdmin;
|
||||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||||
_updateLocalUserInfo();
|
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
// ugly here, tmp solution
|
// ugly here, tmp solution
|
||||||
bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');
|
bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');
|
||||||
|
|||||||
@@ -2034,5 +2034,9 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
||||||
|
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use crate::{
|
|||||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
||||||
kcp_stream::KcpStream,
|
kcp_stream::KcpStream,
|
||||||
secure_tcp,
|
secure_tcp,
|
||||||
ui_interface::{get_builtin_option, use_texture_render},
|
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
@@ -2638,6 +2638,7 @@ impl LoginConfigHandler {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
avatar = resolve_avatar_url(avatar);
|
||||||
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
||||||
if display_name.is_empty() {
|
if display_name.is_empty() {
|
||||||
display_name =
|
display_name =
|
||||||
|
|||||||
@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
|
|||||||
get_api_server()
|
get_api_server()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
||||||
|
SyncReturn(resolve_avatar_url(avatar))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
||||||
http_request(url, method, body, header)
|
http_request(url, method, body, header)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
||||||
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
||||||
|
|
||||||
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
||||||
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
||||||
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
||||||
|
|||||||
@@ -1584,6 +1584,6 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn verify_ffi_enum_data_size() {
|
fn verify_ffi_enum_data_size() {
|
||||||
println!("{}", std::mem::size_of::<Data>());
|
println!("{}", std::mem::size_of::<Data>());
|
||||||
assert!(std::mem::size_of::<Data>() <= 96);
|
assert!(std::mem::size_of::<Data>() <= 120);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,39 +245,20 @@ pub fn get_builtin_option(key: &str) -> String {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_local_option(key: String, value: String) {
|
pub fn set_local_option(key: String, value: String) {
|
||||||
let value = normalize_local_option_value(&key, value);
|
|
||||||
LocalConfig::set_option(key.clone(), value);
|
LocalConfig::set_option(key.clone(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_local_option_value(key: &str, value: String) -> String {
|
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
|
||||||
if key != "user_info" || value.is_empty() {
|
/// by prepending the API server address.
|
||||||
return value;
|
pub fn resolve_avatar_url(avatar: String) -> String {
|
||||||
|
let avatar = avatar.trim().to_owned();
|
||||||
|
if avatar.starts_with('/') {
|
||||||
|
let api_server = get_api_server();
|
||||||
|
if !api_server.is_empty() {
|
||||||
|
return format!("{}{}", api_server.trim_end_matches('/'), avatar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&value) else {
|
avatar
|
||||||
return value;
|
|
||||||
};
|
|
||||||
let Some(obj) = v.as_object_mut() else {
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
let Some(avatar) = obj
|
|
||||||
.get("avatar")
|
|
||||||
.and_then(|x| x.as_str())
|
|
||||||
.map(|x| x.trim().to_owned())
|
|
||||||
else {
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
if !avatar.starts_with('/') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
let api_server = get_api_server();
|
|
||||||
if api_server.is_empty() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
obj.insert(
|
|
||||||
"avatar".to_owned(),
|
|
||||||
serde_json::Value::String(format!("{}{}", api_server.trim_end_matches('/'), avatar)),
|
|
||||||
);
|
|
||||||
serde_json::to_string(&v).unwrap_or(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
|
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
|
||||||
|
|||||||
Reference in New Issue
Block a user