From 9111bfc1de23340047877d1eb3ae91d3532b6215 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Feb 2026 09:43:55 +0800 Subject: [PATCH] - UI display: display_name first - Fallback: name - Technical identity: still name ### What changed - Added account display helpers and display_name state in user model: - flutter/lib/models/user_model.dart:16 - Account/logout label now uses display_name (@name) when both exist: - flutter/lib/mobile/pages/settings_page.dart:689 - flutter/lib/desktop/pages/desktop_setting_page.dart:2016 - flutter/lib/desktop/pages/desktop_setting_page.dart:2135 - Desktop Account info now shows both when applicable: - Display Name: ... - Username: ... - flutter/lib/desktop/pages/desktop_setting_page.dart:2039 - Previously done group-list behavior remains: - group user list displays display_name with name fallback - flutter/lib/common/widgets/my_group.dart:187 - Persistence path for display_name remains enabled (including group cache/submodule field): - libs/hbb_common/src/config.rs:2347 - src/client.rs:2630 - LoginRequest.my_name now resolves as: 1. OPTION_DISPLAY_NAME (manual override) 2. user_info.display_name 3. user_info.name 4. OS username fallback --- flutter/lib/common/hbbs/hbbs.dart | 8 ++++++ flutter/lib/common/widgets/my_group.dart | 11 ++++---- .../desktop/pages/desktop_setting_page.dart | 11 ++++++-- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/models/user_model.dart | 19 +++++++++++++- libs/hbb_common | 2 +- src/client.rs | 8 +++--- src/hbbs_http/account.rs | 9 ++++++- src/ui/index.tis | 25 ++++++++++++++++--- 9 files changed, 78 insertions(+), 17 deletions(-) diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index aab8ba597..f3b210184 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -25,6 +25,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified } // Is all the fields of the user needed? class UserPayload { String name = ''; + String displayName = ''; String email = ''; String note = ''; String? verifier; @@ -33,6 +34,7 @@ class UserPayload { UserPayload.fromJson(Map json) : name = json['name'] ?? '', + displayName = json['display_name'] ?? '', email = json['email'] ?? '', note = json['note'] ?? '', verifier = json['verifier'], @@ -46,6 +48,7 @@ class UserPayload { Map toJson() { final Map map = { 'name': name, + 'display_name': displayName, 'status': status == UserStatus.kDisabled ? 0 : status == UserStatus.kUnverified @@ -58,9 +61,14 @@ class UserPayload { Map toGroupCacheJson() { final Map map = { 'name': name, + 'display_name': displayName, }; return map; } + + String get displayNameOrName { + return displayName.trim().isEmpty ? name : displayName; + } } class PeerPayload { diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 6207a7363..572be5f21 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -158,9 +158,9 @@ class _MyGroupState extends State { return Obx(() { final userItems = gFFI.groupModel.users.where((p0) { if (searchAccessibleItemNameText.isNotEmpty) { - return p0.name - .toLowerCase() - .contains(searchAccessibleItemNameText.value.toLowerCase()); + final search = searchAccessibleItemNameText.value.toLowerCase(); + return p0.name.toLowerCase().contains(search) || + p0.displayNameOrName.toLowerCase().contains(search); } return true; }).toList(); @@ -187,6 +187,7 @@ class _MyGroupState extends State { Widget _buildUserItem(UserPayload user) { final username = user.name; + final displayName = user.displayNameOrName; return InkWell(onTap: () { isSelectedDeviceGroup.value = false; if (selectedAccessibleItemName.value != username) { @@ -229,7 +230,7 @@ class _MyGroupState extends State { ), ), ).marginOnly(right: 4), - if (isMe) Flexible(child: Text(username)), + if (isMe) Flexible(child: Text(displayName)), if (isMe) Flexible( child: Container( @@ -246,7 +247,7 @@ class _MyGroupState extends State { ), ), ), - if (!isMe) Expanded(child: Text(username)), + if (!isMe) Expanded(child: Text(displayName)), ], ).paddingSymmetric(vertical: 4), ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b26d909cb..918ea5220 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2016,7 +2016,9 @@ class _AccountState extends State<_Account> { Widget accountAction() { return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : 'Logout (${gFFI.userModel.accountLabelWithHandle})', () => { gFFI.userModel.userName.value.isEmpty ? loginDialog() @@ -2037,6 +2039,9 @@ class _AccountState extends State<_Account> { offstage: gFFI.userModel.userName.value.isEmpty, child: Column( children: [ + if (gFFI.userModel.displayName.value.trim().isNotEmpty && + gFFI.userModel.displayName.value != gFFI.userModel.userName.value) + text('Display Name', gFFI.userModel.displayName.value), text('Username', gFFI.userModel.userName.value), // text('Group', gFFI.groupModel.groupName.value), ], @@ -2130,7 +2135,9 @@ class _PluginState extends State<_Plugin> { Widget accountAction() { return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : 'Logout (${gFFI.userModel.accountLabelWithHandle})', () => { gFFI.userModel.userName.value.isEmpty ? loginDialog() diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c2e2ef57d..afd3422d7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -688,7 +688,7 @@ class _SettingsState extends State with WidgetsBindingObserver { SettingsTile( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty ? translate('Login') - : '${translate('Logout')} (${gFFI.userModel.userName.value})')), + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')), leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 217d74aee..c850c4cf6 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -16,9 +16,23 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; + final RxString displayName = ''.obs; final RxBool isAdmin = false.obs; final RxString networkError = ''.obs; bool get isLogin => userName.isNotEmpty; + String get displayNameOrUserName => + displayName.value.trim().isEmpty ? userName.value : displayName.value; + String get accountLabelWithHandle { + final username = userName.value.trim(); + if (username.isEmpty) { + return ''; + } + final preferred = displayName.value.trim(); + if (preferred.isEmpty || preferred == username) { + return username; + } + return '$preferred (@$username)'; + } WeakReference parent; UserModel(this.parent) { @@ -98,7 +112,8 @@ class UserModel { _updateLocalUserInfo() { final userInfo = getLocalUserInfo(); if (userInfo != null) { - userName.value = userInfo['name']; + userName.value = (userInfo['name'] ?? '').toString(); + displayName.value = (userInfo['display_name'] ?? '').toString(); } } @@ -110,10 +125,12 @@ class UserModel { await gFFI.groupModel.reset(); } userName.value = ''; + displayName.value = ''; } _parseAndUpdateUser(UserPayload user) { userName.value = user.name; + displayName.value = user.displayName; isAdmin.value = user.isAdmin; bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user)); if (isWeb) { diff --git a/libs/hbb_common b/libs/hbb_common index da339dca6..0b60b9ffa 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit da339dca64ecae3273838c0a1395c7fe2f1a1016 +Subproject commit 0b60b9ffa05259f72cd33e79010ef8e15d42b851 diff --git a/src/client.rs b/src/client.rs index a7b681ee1..aad03a726 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2630,10 +2630,12 @@ impl LoginConfigHandler { display_name = serde_json::from_str::(&LocalConfig::get_option("user_info")) .map(|x| { - x.get("name") - .map(|x| x.as_str().unwrap_or_default()) + x.get("display_name") + .and_then(|x| x.as_str()) + .filter(|x| !x.is_empty()) + .or_else(|| x.get("name").and_then(|x| x.as_str())) + .map(|x| x.to_owned()) .unwrap_or_default() - .to_owned() }) .unwrap_or_default(); } diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 6bdef6f06..6644aee28 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -80,6 +80,8 @@ pub enum UserStatus { pub struct UserPayload { pub name: String, #[serde(default)] + pub display_name: Option, + #[serde(default)] pub email: Option, #[serde(default)] pub note: Option, @@ -268,7 +270,12 @@ impl OidcSession { ); LocalConfig::set_option( "user_info".to_owned(), - serde_json::json!({ "name": auth_body.user.name, "status": auth_body.user.status }).to_string(), + serde_json::json!({ + "name": auth_body.user.name, + "display_name": auth_body.user.display_name, + "status": auth_body.user.status + }) + .to_string(), ); } } diff --git a/src/ui/index.tis b/src/ui/index.tis index 09aa0c306..d4934ba0b 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -358,6 +358,22 @@ function getUserName() { return ''; } +function getAccountLabelWithHandle() { + try { + var user = JSON.parse(handler.get_local_option("user_info")); + var username = (user.name || '').trim(); + if (!username) { + return ''; + } + var displayName = (user.display_name || '').trim(); + if (!displayName || displayName == username) { + return username; + } + return displayName + " (@" + username + ")"; + } catch(e) {} + return ''; +} + // Shared dialog functions function open_custom_server_dialog() { var configOptions = handler.get_options(); @@ -493,7 +509,7 @@ class MyIdMenu: Reactor.Component { } function renderPop() { - var username = handler.get_local_option("access_token") ? getUserName() : ''; + var accountLabel = handler.get_local_option("access_token") ? getAccountLabelWithHandle() : ''; return {!disable_settings &&
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • } @@ -521,8 +537,8 @@ class MyIdMenu: Reactor.Component { {!disable_settings && } {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } {!disable_change_id && handler.is_ok_change_id() ?
    : ""} - {!disable_account && (username ? -
  • {translate('Logout')} ({username})
  • : + {!disable_account && (accountLabel ? +
  • {translate('Logout')} ({accountLabel})
  • :
  • {translate('Login')}
  • )} {!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""}
    @@ -1430,6 +1446,9 @@ checkConnectStatus(); function set_local_user_info(user) { var user_info = {name: user.name}; + if (user.display_name) { + user_info.display_name = user.display_name; + } if (user.status) { user_info.status = user.status; }