Compare commits

...

8 Commits

Author SHA1 Message Date
21pages
743705545a increase ipc Data enum size limit to 120 bytes
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-04 18:28:39 +08:00
21pages
89a34642a3 web: implement mainResolveAvatarUrl via js getByName
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-04 18:17:05 +08:00
21pages
d9e2107fb8 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>
2026-03-04 15:58:20 +08:00
rustdesk
890282e385 avatar 2026-03-02 12:27:21 +08:00
rustdesk
157dbdc543 fix avatar in hbb_common 2026-03-02 12:14:26 +08:00
rustdesk
6ba23683d5 avatar in libs/hbb_comon 2026-03-02 12:06:20 +08:00
fufesou
80a5865db3 macOS update: restore LaunchAgent in GUI session and isolate temp update dir by euid (#14434)
* fix(update): macos, load agent

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): macos, isolate temp update dir by euid

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact(update): macos script

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-01 20:06:04 +08:00
MichaIng
9cb6f38aea packaging: deb: remove obsolete Python version check (#14429)
It was used to conditionally install a Python module in the past. But that is not the case anymore since https://github.com/rustdesk/rustdesk/commit/37dbfcc. Now the check is obsolete.

Due to `set -e`, the check leads to a package configuration failure if Python is not installed, which however otherwise is not needed for RustDesk.

The commit includes an indentation fix and trailing space removal.

Signed-off-by: MichaIng <micha@dietpi.com>
2026-03-01 18:05:19 +08:00
24 changed files with 256 additions and 80 deletions

View File

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

View File

@@ -26,6 +26,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
class UserPayload { class UserPayload {
String name = ''; String name = '';
String displayName = ''; String displayName = '';
String avatar = '';
String email = ''; String email = '';
String note = ''; String note = '';
String? verifier; String? verifier;
@@ -35,6 +36,7 @@ class UserPayload {
UserPayload.fromJson(Map<String, dynamic> json) UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '', : name = json['name'] ?? '',
displayName = json['display_name'] ?? '', displayName = json['display_name'] ?? '',
avatar = json['avatar'] ?? '',
email = json['email'] ?? '', email = json['email'] ?? '',
note = json['note'] ?? '', note = json['note'] ?? '',
verifier = json['verifier'], verifier = json['verifier'],
@@ -49,6 +51,7 @@ class UserPayload {
final Map<String, dynamic> map = { final Map<String, dynamic> map = {
'name': name, 'name': name,
'display_name': displayName, 'display_name': displayName,
'avatar': avatar,
'status': status == UserStatus.kDisabled 'status': status == UserStatus.kDisabled
? 0 ? 0
: status == UserStatus.kUnverified : status == UserStatus.kUnverified

View File

@@ -2026,28 +2026,65 @@ class _AccountState extends State<_Account> {
} }
Widget useInfo() { Widget useInfo() {
text(String key, String value) {
return Align(
alignment: Alignment.centerLeft,
child: SelectionArea(child: Text('${translate(key)}: $value'))
.marginSymmetric(vertical: 4),
);
}
return Obx(() => Offstage( return Obx(() => Offstage(
offstage: gFFI.userModel.userName.value.isEmpty, offstage: gFFI.userModel.userName.value.isEmpty,
child: Column( child: Container(
children: [ padding: const EdgeInsets.all(12),
if (gFFI.userModel.displayName.value.trim().isNotEmpty && decoration: BoxDecoration(
gFFI.userModel.displayName.value.trim() != color: Theme.of(context).colorScheme.surfaceContainerHighest,
gFFI.userModel.userName.value.trim()) borderRadius: BorderRadius.circular(10),
text('Display Name', gFFI.userModel.displayName.value.trim()), ),
text('Username', gFFI.userModel.userName.value), child: Builder(builder: (context) {
// text('Group', gFFI.groupModel.groupName.value), final avatarWidget = _buildUserAvatar();
], return Row(
children: [
if (avatarWidget != null) avatarWidget,
if (avatarWidget != null) const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
gFFI.userModel.displayNameOrUserName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
SelectionArea(
child: Text(
'@${gFFI.userModel.userName.value}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color:
Theme.of(context).textTheme.bodySmall?.color,
),
),
),
],
),
),
],
);
}),
), ),
)).marginOnly(left: 18, top: 16); )).marginOnly(left: 18, top: 16);
} }
Widget? _buildUserAvatar() {
// Resolve relative avatar path at display time
final avatar =
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
return buildAvatarWidget(
avatar: avatar,
size: 44,
);
}
} }
class _Checkbox extends StatefulWidget { class _Checkbox extends StatefulWidget {

View File

@@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader>
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( _buildClientAvatar().marginOnly(right: 10.0),
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: str2color(client.name),
borderRadius: BorderRadius.circular(15.0),
),
child: Text(
client.name[0],
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 55,
),
),
).marginOnly(right: 10.0),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -582,6 +566,35 @@ class _CmHeaderState extends State<_CmHeader>
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
Widget _buildClientAvatar() {
return buildAvatarWidget(
avatar: client.avatar,
size: 70,
borderRadius: 15,
fallback: _buildInitialAvatar(),
)!;
}
Widget _buildInitialAvatar() {
return Container(
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: str2color(client.name),
borderRadius: BorderRadius.circular(15.0),
),
child: Text(
client.name.isNotEmpty ? client.name[0] : '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 55,
),
),
);
}
} }
class _PrivilegeBoard extends StatefulWidget { class _PrivilegeBoard extends StatefulWidget {

View File

@@ -841,13 +841,7 @@ class ClientInfo extends StatelessWidget {
flex: -1, flex: -1,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
child: CircleAvatar( child: _buildAvatar(context))),
backgroundColor: str2color(
client.name,
Theme.of(context).brightness == Brightness.light
? 255
: 150),
child: Text(client.name[0])))),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -860,6 +854,20 @@ class ClientInfo extends StatelessWidget {
), ),
])); ]));
} }
Widget _buildAvatar(BuildContext context) {
final fallback = CircleAvatar(
backgroundColor: str2color(
client.name,
Theme.of(context).brightness == Brightness.light ? 255 : 150),
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
);
return buildAvatarWidget(
avatar: client.avatar,
size: 40,
fallback: fallback,
)!;
}
} }
void androidChannelInit() { void androidChannelInit() {

View File

@@ -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();

View File

@@ -820,6 +820,7 @@ class Client {
bool isTerminal = false; bool isTerminal = false;
String portForward = ""; String portForward = "";
String name = ""; String name = "";
String avatar = "";
String peerId = ""; // peer user's id,show at app String peerId = ""; // peer user's id,show at app
bool keyboard = false; bool keyboard = false;
bool clipboard = false; bool clipboard = false;
@@ -847,6 +848,7 @@ class Client {
isTerminal = json['is_terminal'] ?? false; isTerminal = json['is_terminal'] ?? false;
portForward = json['port_forward']; portForward = json['port_forward'];
name = json['name']; name = json['name'];
avatar = json['avatar'] ?? '';
peerId = json['peer_id']; peerId = json['peer_id'];
keyboard = json['keyboard']; keyboard = json['keyboard'];
clipboard = json['clipboard']; clipboard = json['clipboard'];
@@ -870,6 +872,7 @@ class Client {
data['is_terminal'] = isTerminal; data['is_terminal'] = isTerminal;
data['port_forward'] = portForward; data['port_forward'] = portForward;
data['name'] = name; data['name'] = name;
data['avatar'] = avatar;
data['peer_id'] = peerId; data['peer_id'] = peerId;
data['keyboard'] = keyboard; data['keyboard'] = keyboard;
data['clipboard'] = clipboard; data['clipboard'] = clipboard;

View File

@@ -17,6 +17,7 @@ bool refreshingUser = false;
class UserModel { class UserModel {
final RxString userName = ''.obs; final RxString userName = ''.obs;
final RxString displayName = ''.obs; final RxString displayName = ''.obs;
final RxString avatar = ''.obs;
final RxBool isAdmin = false.obs; final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs; final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty; bool get isLogin => userName.isNotEmpty;
@@ -33,6 +34,7 @@ class UserModel {
} }
return '$preferred (@$username)'; return '$preferred (@$username)';
} }
WeakReference<FFI> parent; WeakReference<FFI> parent;
UserModel(this.parent) { UserModel(this.parent) {
@@ -114,6 +116,7 @@ class UserModel {
if (userInfo != null) { if (userInfo != null) {
userName.value = (userInfo['name'] ?? '').toString(); userName.value = (userInfo['name'] ?? '').toString();
displayName.value = (userInfo['display_name'] ?? '').toString(); displayName.value = (userInfo['display_name'] ?? '').toString();
avatar.value = (userInfo['avatar'] ?? '').toString();
} }
} }
@@ -126,11 +129,13 @@ class UserModel {
} }
userName.value = ''; userName.value = '';
displayName.value = ''; displayName.value = '';
avatar.value = '';
} }
_parseAndUpdateUser(UserPayload user) { _parseAndUpdateUser(UserPayload user) {
userName.value = user.name; userName.value = user.name;
displayName.value = user.displayName; displayName.value = user.displayName;
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));
if (isWeb) { if (isWeb) {

View File

@@ -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() {}
} }

View File

@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
if [ "systemd" == "$INITSYS" ]; then if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then if [ -e /etc/systemd/system/rustdesk.service ]; then
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') mkdir -p /usr/lib/systemd/system/
parsedVersion=$(echo "${version//./}")
mkdir -p /usr/lib/systemd/system/
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
# try fix error in Ubuntu 18.04 # try fix error in Ubuntu 18.04
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error. # Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.

View File

@@ -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")]
@@ -2625,6 +2625,20 @@ impl LoginConfigHandler {
} else { } else {
(my_id, self.id.clone()) (my_id, self.id.clone())
}; };
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
if avatar.is_empty() {
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
"user_info",
))
.ok()
.and_then(|x| {
x.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
})
.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 =
@@ -2684,6 +2698,7 @@ impl LoginConfigHandler {
}) })
.into(), .into(),
hwid, hwid,
avatar,
..Default::default() ..Default::default()
}; };
match self.conn_type { match self.conn_type {

View File

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

View File

@@ -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";
@@ -82,6 +83,8 @@ pub struct UserPayload {
#[serde(default)] #[serde(default)]
pub display_name: Option<String>, pub display_name: Option<String>,
#[serde(default)] #[serde(default)]
pub avatar: Option<String>,
#[serde(default)]
pub email: Option<String>, pub email: Option<String>,
#[serde(default)] #[serde(default)]
pub note: Option<String>, pub note: Option<String>,
@@ -273,6 +276,7 @@ impl OidcSession {
serde_json::json!({ serde_json::json!({
"name": auth_body.user.name, "name": auth_body.user.name,
"display_name": auth_body.user.display_name, "display_name": auth_body.user.display_name,
"avatar": auth_body.user.avatar,
"status": auth_body.user.status "status": auth_body.user.status
}) })
.to_string(), .to_string(),

View File

@@ -226,6 +226,7 @@ pub enum Data {
is_terminal: bool, is_terminal: bool,
peer_id: String, peer_id: String,
name: String, name: String,
avatar: String,
authorized: bool, authorized: bool,
port_forward: String, port_forward: String,
keyboard: bool, keyboard: bool,
@@ -1583,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);
} }
} }

View File

@@ -42,9 +42,16 @@ static PRIVILEGES_SCRIPTS_DIR: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
static mut LATEST_SEED: i32 = 0; static mut LATEST_SEED: i32 = 0;
// Using a fixed temporary directory for updates is preferable to #[inline]
// using one that includes the custom client name. fn get_update_temp_dir() -> PathBuf {
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; let euid = unsafe { hbb_common::libc::geteuid() };
Path::new("/tmp").join(format!(".rustdeskupdate-{}", euid))
}
#[inline]
fn get_update_temp_dir_string() -> String {
get_update_temp_dir().to_string_lossy().into_owned()
}
/// Global mutex to serialize CoreGraphics cursor operations. /// Global mutex to serialize CoreGraphics cursor operations.
/// This prevents race conditions between cursor visibility (hide depth tracking) /// This prevents race conditions between cursor visibility (hide depth tracking)
@@ -285,21 +292,6 @@ fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync
_ => { _ => {
let installed = std::path::Path::new(&agent_plist_file).exists(); let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", &agent_plist_file, installed); log::info!("Agent file {} installed: {}", &agent_plist_file, installed);
if installed {
// Unload first, or load may not work if already loaded.
// We hope that the load operation can immediately trigger a start.
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
let status = std::process::Command::new("launchctl")
.args(&["load", "-w", &agent_plist_file])
.status();
log::info!("launch server, status: {:?}", &status);
}
} }
} }
}; };
@@ -418,7 +410,9 @@ pub fn set_cursor_pos(x: i32, y: i32) -> bool {
let _guard = match CG_CURSOR_MUTEX.try_lock() { let _guard = match CG_CURSOR_MUTEX.try_lock() {
Ok(guard) => guard, Ok(guard) => guard,
Err(std::sync::TryLockError::WouldBlock) => { Err(std::sync::TryLockError::WouldBlock) => {
log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"); log::error!(
"[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"
);
debug_assert!(false, "Re-entrant call to set_cursor_pos detected"); debug_assert!(false, "Re-entrant call to set_cursor_pos detected");
return false; return false;
} }
@@ -825,7 +819,8 @@ pub fn quit_gui() {
#[inline] #[inline]
pub fn try_remove_temp_update_dir(dir: Option<&str>) { pub fn try_remove_temp_update_dir(dir: Option<&str>) {
let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR)); let target_path_buf = dir.map(PathBuf::from).unwrap_or_else(get_update_temp_dir);
let target_path = target_path_buf.as_path();
if target_path.exists() { if target_path.exists() {
std::fs::remove_dir_all(target_path).ok(); std::fs::remove_dir_all(target_path).ok();
} }
@@ -901,25 +896,28 @@ end run
} }
pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> {
let update_temp_dir = get_update_temp_dir_string();
println!("Starting update from DMG: {}", dmg_path); println!("Starting update from DMG: {}", dmg_path);
extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; extract_dmg(dmg_path, &update_temp_dir)?;
println!("DMG extracted"); println!("DMG extracted");
update_extracted(UPDATE_TEMP_DIR)?; update_extracted(&update_temp_dir)?;
println!("Update process started"); println!("Update process started");
Ok(()) Ok(())
} }
pub fn update_to(_file: &str) -> ResultType<()> { pub fn update_to(_file: &str) -> ResultType<()> {
update_extracted(UPDATE_TEMP_DIR)?; let update_temp_dir = get_update_temp_dir_string();
update_extracted(&update_temp_dir)?;
Ok(()) Ok(())
} }
pub fn extract_update_dmg(file: &str) { pub fn extract_update_dmg(file: &str) {
let update_temp_dir = get_update_temp_dir_string();
let mut evt: HashMap<&str, String> = let mut evt: HashMap<&str, String> =
HashMap::from([("name", "extract-update-dmg".to_string())]); HashMap::from([("name", "extract-update-dmg".to_string())]);
match extract_dmg(file, UPDATE_TEMP_DIR) { match extract_dmg(file, &update_temp_dir) {
Ok(_) => { Ok(_) => {
log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); log::info!("Extracted dmg file to {}", update_temp_dir);
} }
Err(e) => { Err(e) => {
evt.insert("err", e.to_string()); evt.insert("err", e.to_string());

View File

@@ -14,8 +14,12 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir}
set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";" set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";"
set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";" set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";"
set load_service to "launchctl load -w " & daemon_plist & ";" set load_service to "launchctl load -w " & daemon_plist & ";"
set agent_label_cmd to "agent_label=$(basename " & quoted form of agent_plist & " .plist);"
set bootstrap_agent to "if [ -n \"$uid\" ]; then launchctl bootstrap gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootstrap user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl load -w " & quoted form of agent_plist & " || true; else launchctl load -w " & quoted form of agent_plist & " || true; fi;"
set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;"
set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent
set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
end run end run

View File

@@ -1813,6 +1813,7 @@ impl Connection {
port_forward: self.port_forward_address.clone(), port_forward: self.port_forward_address.clone(),
peer_id, peer_id,
name, name,
avatar: self.lr.avatar.clone(),
authorized, authorized,
keyboard: self.keyboard, keyboard: self.keyboard,
clipboard: self.clipboard, clipboard: self.clipboard,

View File

@@ -57,6 +57,11 @@ div.icon {
font-weight: bold; font-weight: bold;
} }
img.icon {
size: 96px;
border-radius: 8px;
}
div.id { div.id {
@ELLIPSIS; @ELLIPSIS;
color: color(green-blue); color: color(green-blue);

View File

@@ -28,6 +28,7 @@ impl InvokeUiCM for SciterHandler {
client.port_forward.clone(), client.port_forward.clone(),
client.peer_id.clone(), client.peer_id.clone(),
client.name.clone(), client.name.clone(),
client.avatar.clone(),
client.authorized, client.authorized,
client.keyboard, client.keyboard,
client.clipboard, client.clipboard,

View File

@@ -42,9 +42,11 @@ class Body: Reactor.Component
return <div .content style="size:*"> return <div .content style="size:*">
<div .left-panel> <div .left-panel>
<div .icon-and-id> <div .icon-and-id>
{c.avatar ?
<img .icon src={c.avatar} /> :
<div .icon style={"background: " + string2RGB(c.name, 1)}> <div .icon style={"background: " + string2RGB(c.name, 1)}>
{c.name[0].toUpperCase()} {c.name[0].toUpperCase()}
</div> </div>}
<div> <div>
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div> <div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
<div .id>({c.peer_id})</div> <div .id>({c.peer_id})</div>
@@ -366,7 +368,7 @@ function bring_to_top(idx=-1) {
} }
} }
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
stdout.println("new connection #" + id + ": " + peer_id); stdout.println("new connection #" + id + ": " + peer_id);
var conn; var conn;
connections.map(function(c) { connections.map(function(c) {
@@ -385,6 +387,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
conn = { conn = {
id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id,
port_forward: port_forward, port_forward: port_forward,
avatar: avatar,
name: name, authorized: authorized, time: new Date(), now: new Date(), name: name, authorized: authorized, time: new Date(), now: new Date(),
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
audio: audio, file: file, restart: restart, recording: recording, audio: audio, file: file, restart: restart, recording: recording,

View File

@@ -1451,6 +1451,9 @@ function set_local_user_info(user) {
if (user.display_name) { if (user.display_name) {
user_info.display_name = user.display_name; user_info.display_name = user.display_name;
} }
if (user.avatar) {
user_info.avatar = user.avatar;
}
if (user.status) { if (user.status) {
user_info.status = user.status; user_info.status = user.status;
} }

View File

@@ -134,6 +134,7 @@ pub struct Client {
pub is_terminal: bool, pub is_terminal: bool,
pub port_forward: String, pub port_forward: String,
pub name: String, pub name: String,
pub avatar: String,
pub peer_id: String, pub peer_id: String,
pub keyboard: bool, pub keyboard: bool,
pub clipboard: bool, pub clipboard: bool,
@@ -220,6 +221,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
port_forward: String, port_forward: String,
peer_id: String, peer_id: String,
name: String, name: String,
avatar: String,
authorized: bool, authorized: bool,
keyboard: bool, keyboard: bool,
clipboard: bool, clipboard: bool,
@@ -240,6 +242,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
is_terminal, is_terminal,
port_forward, port_forward,
name: name.clone(), name: name.clone(),
avatar,
peer_id: peer_id.clone(), peer_id: peer_id.clone(),
keyboard, keyboard,
clipboard, clipboard,
@@ -500,9 +503,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
} }
Ok(Some(data)) => { Ok(Some(data)) => {
match data { match data {
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
log::debug!("conn_id: {}", id); log::debug!("conn_id: {}", id);
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.conn_id = id; self.conn_id = id;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@@ -823,6 +826,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward, port_forward,
peer_id, peer_id,
name, name,
avatar,
authorized, authorized,
keyboard, keyboard,
clipboard, clipboard,
@@ -843,6 +847,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward, port_forward,
peer_id, peer_id,
name, name,
avatar,
authorized, authorized,
keyboard, keyboard,
clipboard, clipboard,

View File

@@ -245,7 +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) {
LocalConfig::set_option(key.clone(), value.clone()); LocalConfig::set_option(key.clone(), value);
}
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
/// by prepending the API server address.
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);
}
}
avatar
} }
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]