Compare commits

...

3 Commits

Author SHA1 Message Date
fufesou
88ae00ba73 refact: restart remote device, autoconnect (#15290)
* refact: restart remote device, autoconnect

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

* fix: guard restart reconnect timer after session close

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

* Simple refactor

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

* fix(restart): auto connect, comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-17 17:50:18 +08:00
Maison da Silva
7c26575dbd Update translation for remote toolbar docking message (#15297)
* Update translation for remote toolbar docking message

Update translation for remote toolbar docking message

* Translate 'Display' to 'Tela' in Portuguese locale

* Change translation of 'Display' to 'Exibição'
2026-06-17 17:36:27 +08:00
fufesou
93d064a9b0 refact(oidc): icon azure to microsoft (#15278)
* refact(oidc): icon azure to microsoft

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

* Simple refactor

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

* refact: oidc, remove unused auth-azure.svg

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-17 14:36:53 +08:00
7 changed files with 127 additions and 22 deletions

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199"><path fill="#0089d6" d="M118.432 187.698c32.89-5.81 60.055-10.618 60.367-10.684l.568-.12-31.052-36.935c-17.078-20.314-31.051-37.014-31.051-37.11 0-.182 32.063-88.477 32.243-88.792.06-.105 21.88 37.567 52.893 91.32 29.035 50.323 52.973 91.815 53.195 92.203l.405.707-98.684-.012-98.684-.013 59.8-10.564zM0 176.435c0-.052 14.631-25.451 32.514-56.442l32.514-56.347 37.891-31.799C123.76 14.358 140.867.027 140.935.001c.069-.026-.205.664-.609 1.534s-18.919 40.582-41.145 88.25l-40.41 86.67-29.386.037c-16.162.02-29.385-.005-29.385-.057z"/></svg>

Before

Width:  |  Height:  |  Size: 604 B

View File

@@ -24,6 +24,35 @@ const kOpSvgList = [
'microsoft' 'microsoft'
]; ];
class _OidcProviderBranding {
final String label;
final String iconKey;
const _OidcProviderBranding({
required this.label,
required this.iconKey,
});
}
_OidcProviderBranding _oidcProviderBranding(String op) {
switch (op.toLowerCase()) {
case 'azure':
return _OidcProviderBranding(
label: 'Microsoft',
iconKey: 'microsoft',
);
default:
return _OidcProviderBranding(
label: {
'github': 'GitHub',
'gitlab': 'GitLab',
}[op.toLowerCase()] ??
toCapitalized(op),
iconKey: op.toLowerCase(),
);
}
}
class _IconOP extends StatelessWidget { class _IconOP extends StatelessWidget {
final String op; final String op;
final String? icon; final String? icon;
@@ -74,11 +103,8 @@ class ButtonOP extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final opLabel = { final branding = _oidcProviderBranding(op);
'github': 'GitHub', final buttonLabel = translate("Continue with {${branding.label}}");
'gitlab': 'GitLab'
}[op.toLowerCase()] ??
toCapitalized(op);
return Row(children: [ return Row(children: [
Container( Container(
height: height, height: height,
@@ -95,7 +121,7 @@ class ButtonOP extends StatelessWidget {
SizedBox( SizedBox(
width: 30, width: 30,
child: _IconOP( child: _IconOP(
op: op, op: branding.iconKey,
icon: icon, icon: icon,
margin: EdgeInsets.only(right: 5), margin: EdgeInsets.only(right: 5),
), ),
@@ -103,8 +129,7 @@ class ButtonOP extends StatelessWidget {
Expanded( Expanded(
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Center( child: Center(child: Text(buttonLabel)),
child: Text(translate("Continue with {$opLabel}"))),
), ),
), ),
], ],

View File

@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id); typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool); typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
final _constSessionId = Uuid().v4obj(); final _constSessionId = Uuid().v4obj();
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
const _restartReconnectSilentDelaySecs = 5;
class CachedPeerData { class CachedPeerData {
Map<String, dynamic> updatePrivacyMode = {}; Map<String, dynamic> updatePrivacyMode = {};
@@ -119,6 +121,7 @@ class FfiModel with ChangeNotifier {
bool _touchMode = false; bool _touchMode = false;
late VirtualMouseMode virtualMouseMode; late VirtualMouseMode virtualMouseMode;
Timer? _timer; Timer? _timer;
Timer? _restartReconnectDelayTimer;
var _reconnects = 1; var _reconnects = 1;
DateTime? _offlineReconnectStartTime; DateTime? _offlineReconnectStartTime;
bool _viewOnly = false; bool _viewOnly = false;
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
_inputBlocked = false; _inputBlocked = false;
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
resetRestartReconnectState();
clearPermissions(); clearPermissions();
waitForImageTimer?.cancel(); waitForImageTimer?.cancel();
timerScreenshot?.cancel(); timerScreenshot?.cancel();
@@ -341,6 +345,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'connection_ready') { } else if (name == 'connection_ready') {
setConnectionType(peerId, evt['secure'] == 'true', setConnectionType(peerId, evt['secure'] == 'true',
evt['direct'] == 'true', evt['stream_type'] ?? ''); evt['direct'] == 'true', evt['stream_type'] ?? '');
resetRestartReconnectState();
} else if (name == 'switch_display') { } else if (name == 'switch_display') {
// switch display is kept for backward compatibility // switch display is kept for backward compatibility
handleSwitchDisplay(evt, sessionId, peerId); handleSwitchDisplay(evt, sessionId, peerId);
@@ -922,8 +927,28 @@ class FfiModel with ChangeNotifier {
enterUserLoginAndPasswordDialog( enterUserLoginAndPasswordDialog(
sessionId, dialogManager, 'terminal-admin-login-tip', false); sessionId, dialogManager, 'terminal-admin-login-tip', false);
} else if (type == 'restarting') { } else if (type == 'restarting') {
showMsgBox(sessionId, type, title, text, link, false, dialogManager, // Treat restart messages as reconnect control events. Rust still sends
hasCancel: false); // title/text for legacy UI and translation reuse; Flutter keeps the last
// frame briefly, then shows the Connecting overlay.
if (_restartReconnectDelayTimer == null) {
parent.target?.inputModel.setRelativeMouseMode(false);
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
clearPermissions();
// Retry once more after the silent window so restart reconnect attempts
// are spaced by the empirical short cadence instead of only updating UI.
_restartReconnectDelayTimer =
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
_restartReconnectDelayTimer = null;
if (parent.target?.closed == true) {
return;
}
reconnect(dialogManager, sessionId, false);
});
}
} else if (type == 'restarting-show') {
_restartReconnectDelayTimer?.cancel();
_restartReconnectDelayTimer = null;
reconnect(dialogManager, sessionId, false);
} else if (type == 'wait-remote-accept-nook') { } else if (type == 'wait-remote-accept-nook') {
showWaitAcceptDialog(sessionId, type, title, text, dialogManager); showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
} else if (type == 'on-uac' || type == 'on-foreground-elevated') { } else if (type == 'on-uac' || type == 'on-foreground-elevated') {
@@ -949,6 +974,11 @@ class FfiModel with ChangeNotifier {
} }
} }
void resetRestartReconnectState() {
_restartReconnectDelayTimer?.cancel();
_restartReconnectDelayTimer = null;
}
/// Auto-retry check for "Remote desktop is offline" error. /// Auto-retry check for "Remote desktop is offline" error.
/// returns true to auto-retry, false otherwise. /// returns true to auto-retry, false otherwise.
bool shouldAutoRetryOnOffline( bool shouldAutoRetryOnOffline(
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
if (displays.isNotEmpty) { if (displays.isNotEmpty) {
_reconnects = 1; _reconnects = 1;
_offlineReconnectStartTime = null; _offlineReconnectStartTime = null;
resetRestartReconnectState();
waitForFirstImage.value = true; waitForFirstImage.value = true;
isRefreshing = false; isRefreshing = false;
} }
@@ -3666,6 +3697,7 @@ class FFI {
/// Mobile reuse FFI /// Mobile reuse FFI
void mobileReset() { void mobileReset() {
ffiModel.resetRestartReconnectState();
ffiModel.waitForFirstImage.value = true; ffiModel.waitForFirstImage.value = true;
ffiModel.isRefreshing = false; ffiModel.isRefreshing = false;
ffiModel.waitForImageDialogShow.value = true; ffiModel.waitForImageDialogShow.value = true;
@@ -3879,6 +3911,7 @@ class FFI {
} }
if (ffiModel.waitForFirstImage.value == true) { if (ffiModel.waitForFirstImage.value == true) {
ffiModel.waitForFirstImage.value = false; ffiModel.waitForFirstImage.value = false;
ffiModel.resetRestartReconnectState();
dialogManager.dismissAll(); dialogManager.dismissAll();
await canvasModel.updateViewStyle(); await canvasModel.updateViewStyle();
await canvasModel.updateScrollStyle(); await canvasModel.updateScrollStyle();

View File

@@ -96,6 +96,8 @@ pub mod screenshot;
pub const MILLI1: Duration = Duration::from_millis(1); pub const MILLI1: Duration = Duration::from_millis(1);
pub const SEC30: Duration = Duration::from_secs(30); pub const SEC30: Duration = Duration::from_secs(30);
// Empirical restart reconnect grace window.
const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
pub const VIDEO_QUEUE_SIZE: usize = 120; pub const VIDEO_QUEUE_SIZE: usize = 120;
const MAX_DECODE_FAIL_COUNTER: usize = 3; const MAX_DECODE_FAIL_COUNTER: usize = 3;
@@ -1740,7 +1742,10 @@ pub struct LoginConfigHandler {
features: Option<Features>, features: Option<Features>,
pub session_id: u64, // used for local <-> server communication pub session_id: u64, // used for local <-> server communication
pub supported_encoding: SupportedEncoding, pub supported_encoding: SupportedEncoding,
pub restarting_remote_device: bool, restarting_remote_device: bool,
// Start time of the restart grace window. On Windows the peer may briefly
// reconnect before the real reboot disconnect.
restart_remote_device_at: Option<Instant>,
pub force_relay: bool, pub force_relay: bool,
pub direct: Option<bool>, pub direct: Option<bool>,
pub received: bool, pub received: bool,
@@ -1849,7 +1854,7 @@ impl LoginConfigHandler {
} }
self.session_id = sid; self.session_id = sid;
self.supported_encoding = Default::default(); self.supported_encoding = Default::default();
self.restarting_remote_device = false; self.clear_restarting_remote_device();
self.force_relay = self.force_relay =
config::option2bool("force-always-relay", &self.get_option("force-always-relay")) config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|| force_relay || force_relay
@@ -2779,6 +2784,30 @@ impl LoginConfigHandler {
msg_out msg_out
} }
pub fn mark_restarting_remote_device(&mut self) {
self.restarting_remote_device = true;
self.restart_remote_device_at = Some(Instant::now());
}
pub fn clear_restarting_remote_device(&mut self) {
self.restarting_remote_device = false;
self.restart_remote_device_at = None;
}
pub fn is_restarting_remote_device(&self) -> bool {
if !self.restarting_remote_device {
return false;
}
// Keep this flag alive for a short grace window instead of clearing it on
// connection_ready or the first peer bytes. During OS restart the peer can
// briefly reconnect before the real reboot disconnect, and clearing it too
// early would let the next disconnect escape the restart flow and fall back
// to the normal error dialog / manual reconnect path.
self.restart_remote_device_at
.map(|started_at| started_at.elapsed() < RESTART_REMOTE_DEVICE_GRACE)
.unwrap_or(false)
}
pub fn get_conn_token(&self) -> Option<String> { pub fn get_conn_token(&self) -> Option<String> {
if self.password.is_empty() { if self.password.is_empty() {
return None; return None;
@@ -3718,9 +3747,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn on_establish_connection_error(&self, err: String) { fn on_establish_connection_error(&self, err: String) {
let title = "Connection Error"; let title = "Connection Error";
let text = err.to_string(); let text = err.to_string();
let lc = self.get_lch(); let lch = self.get_lch();
let direct = lc.read().unwrap().direct; let (is_restarting, direct, received) = {
let received = lc.read().unwrap().received; let lc = lch.read().unwrap();
(lc.is_restarting_remote_device(), lc.direct, lc.received)
};
if is_restarting {
log::info!("Restart remote device, suppress connection error: {err}");
// Flutter treats this as a reconnect control event. The text is kept
// for legacy UI and existing translation reuse.
self.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
return;
}
let mut relay_hint = false; let mut relay_hint = false;
let mut relay_hint_type = "relay-hint"; let mut relay_hint_type = "relay-hint";

View File

@@ -10,6 +10,10 @@ use crate::{
common::get_default_sound_input, common::get_default_sound_input,
ui_session_interface::{InvokeUiSession, Session}, ui_session_interface::{InvokeUiSession, Session},
}; };
// Empirical no-data window before exposing the restart reconnect state to the UI.
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
#[cfg(any( #[cfg(any(
@@ -153,7 +157,6 @@ impl<T: InvokeUiSession> Remote<T> {
} }
}; };
let mut last_recv_time = Instant::now();
let mut received = false; let mut received = false;
let conn_type = if self.handler.is_file_transfer() { let conn_type = if self.handler.is_file_transfer() {
ConnType::FILE_TRANSFER ConnType::FILE_TRANSFER
@@ -219,6 +222,7 @@ impl<T: InvokeUiSession> Remote<T> {
let mut fps_instant = Instant::now(); let mut fps_instant = Instant::now();
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
let mut last_recv_time = Instant::now();
loop { loop {
tokio::select! { tokio::select! {
@@ -244,7 +248,7 @@ impl<T: InvokeUiSession> Remote<T> {
} else { } else {
if self.handler.is_restarting_remote_device() { if self.handler.is_restarting_remote_device() {
log::info!("Restart remote device"); log::info!("Restart remote device");
self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", ""); self.handler.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
} else { } else {
log::info!("Reset by the peer"); log::info!("Reset by the peer");
self.handler.msgbox("error", "Connection Error", "Reset by the peer", ""); self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
@@ -279,6 +283,12 @@ impl<T: InvokeUiSession> Remote<T> {
} }
} }
_ = status_timer.tick() => { _ = status_timer.tick() => {
if self.handler.is_restarting_remote_device()
&& last_recv_time.elapsed() >= RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT
{
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
break;
}
let elapsed = fps_instant.elapsed().as_millis(); let elapsed = fps_instant.elapsed().as_millis();
if elapsed < 1000 { if elapsed < 1000 {
continue; continue;

View File

@@ -430,7 +430,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Strong", "Forte"), ("Strong", "Forte"),
("Switch Sides", "Trocar de lado"), ("Switch Sides", "Trocar de lado"),
("Please confirm if you want to share your desktop?", "Por favor, confirme se você deseja compartilhar sua área de trabalho?"), ("Please confirm if you want to share your desktop?", "Por favor, confirme se você deseja compartilhar sua área de trabalho?"),
("Display", "Display"), ("Display", "Exibição"),
("Default View Style", "Estilo de Visualização Padrão"), ("Default View Style", "Estilo de Visualização Padrão"),
("Default Scroll Style", "Estilo de Rolagem Padrão"), ("Default Scroll Style", "Estilo de Rolagem Padrão"),
("Default Image Quality", "Qualidade de Imagem Padrão"), ("Default Image Quality", "Qualidade de Imagem Padrão"),
@@ -744,7 +744,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "A senha permanente está definida como (oculta)."), ("password-hidden-tip", "A senha permanente está definida como (oculta)."),
("preset-password-in-use-tip", "A senha predefinida está sendo usada."), ("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
("Enable privacy mode", "Habilitar modo de privacidade"), ("Enable privacy mode", "Habilitar modo de privacidade"),
("allow-remote-toolbar-docking-any-edge", "Permitir fixar a barra de ferramentas remota em qualquer borda da janela"), ("allow-remote-toolbar-docking-any-edge", "Fixar a barra de ferramentas remota em qualquer borda da janela"),
("API Token", "Token de API"), ("API Token", "Token de API"),
("Deploy", "Implantar"), ("Deploy", "Implantar"),
("Custom ID (optional)", "ID personalizado (opcional)"), ("Custom ID (optional)", "ID personalizado (opcional)"),

View File

@@ -560,7 +560,7 @@ impl<T: InvokeUiSession> Session<T> {
pub fn restart_remote_device(&self) { pub fn restart_remote_device(&self) {
let mut lc = self.lc.write().unwrap(); let mut lc = self.lc.write().unwrap();
lc.restarting_remote_device = true; lc.mark_restarting_remote_device();
let msg = lc.restart_remote_device(); let msg = lc.restart_remote_device();
self.send(Data::Message(msg)); self.send(Data::Message(msg));
} }
@@ -656,7 +656,7 @@ impl<T: InvokeUiSession> Session<T> {
} }
pub fn is_restarting_remote_device(&self) -> bool { pub fn is_restarting_remote_device(&self) -> bool {
self.lc.read().unwrap().restarting_remote_device self.lc.read().unwrap().is_restarting_remote_device()
} }
#[inline] #[inline]