Make the save-password dialog actually visible
Nothing had ever been saved: every app carried savedAccount=None and the Keychain held no Work entries at all. So right-clicking "Fill saved password" had nothing to recall — it was never a fill bug. The offer was firing correctly on login; its dialog just could not be seen. Two things hid it. The stage was parked only for settingsOpen, so the password offer left the apps where they were — and a native webview paints over anything the shell draws. Every shell dialog needs the park, not just Settings. Parking alone was still not enough. show_only and set_stage each move every webview back to the stage, and the staggered bootstrap calls show_only once per app as it builds them, so the park was undone within milliseconds — the dialog appeared and vanished in a flash. Nothing recorded that the apps were *meant* to be away, so every layout pass raced to put them back. AppState now carries that intent and placement() applies it, so a pass that runs while a dialog is up leaves the apps parked instead of returning them to the stage. Verified end to end against a local login form: the offer appears and stays on an XHR login that fires no submit event, the credential reaches the Keychain, right-click fill puts it back, and a failed login is correctly not offered for saving.
This commit is contained in:
@@ -48,6 +48,13 @@ pub struct AppState {
|
|||||||
pub unread: Mutex<std::collections::HashMap<String, u32>>,
|
pub unread: Mutex<std::collections::HashMap<String, u32>>,
|
||||||
/// Size of the last dialog backdrop still, or why there wasn't one.
|
/// Size of the last dialog backdrop still, or why there wasn't one.
|
||||||
pub last_snapshot: Mutex<String>,
|
pub last_snapshot: Mutex<String>,
|
||||||
|
/// A dialog is up and the apps are parked off screen.
|
||||||
|
///
|
||||||
|
/// Repositioning is not a one-off: webviews are re-laid-out whenever the
|
||||||
|
/// stage moves and once per app as the staggered bootstrap builds them.
|
||||||
|
/// Without somewhere to record that they are meant to be away, the next
|
||||||
|
/// such pass puts them straight back over the dialog.
|
||||||
|
pub parked: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -105,6 +112,7 @@ pub fn build_state(handle: &AppHandle) -> Result<AppState, String> {
|
|||||||
last_notification: Mutex::new(String::new()),
|
last_notification: Mutex::new(String::new()),
|
||||||
unread: Mutex::new(std::collections::HashMap::new()),
|
unread: Mutex::new(std::collections::HashMap::new()),
|
||||||
last_snapshot: Mutex::new(String::new()),
|
last_snapshot: Mutex::new(String::new()),
|
||||||
|
parked: Mutex::new(false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +261,7 @@ pub async fn stage_snapshot(app: AppHandle) -> Option<String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) {
|
pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) {
|
||||||
let stage = *state.stage.lock().unwrap();
|
let stage = *state.stage.lock().unwrap();
|
||||||
|
*state.parked.lock().unwrap() = true;
|
||||||
webviews::hide_all(&app, &state.cfg(), stage);
|
webviews::hide_all(&app, &state.cfg(), stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +270,7 @@ pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) {
|
|||||||
pub fn show_stage(app: AppHandle, state: State<'_, AppState>) {
|
pub fn show_stage(app: AppHandle, state: State<'_, AppState>) {
|
||||||
let cfg = state.cfg();
|
let cfg = state.cfg();
|
||||||
let stage = *state.stage.lock().unwrap();
|
let stage = *state.stage.lock().unwrap();
|
||||||
|
*state.parked.lock().unwrap() = false;
|
||||||
let active = state.active.lock().unwrap().clone();
|
let active = state.active.lock().unwrap().clone();
|
||||||
webviews::show_only(&app, active.as_deref(), &cfg, stage);
|
webviews::show_only(&app, active.as_deref(), &cfg, stage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -944,6 +944,19 @@ fn set_page_visibility(handle: &AppHandle, app_id: &str, visible: bool) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where the apps belong right now.
|
||||||
|
///
|
||||||
|
/// While a dialog is up they belong off screen, and every layout pass has to
|
||||||
|
/// agree — otherwise the pass that runs next puts them back over the dialog.
|
||||||
|
fn placement(handle: &AppHandle, stage: (f64, f64, f64, f64)) -> (f64, f64) {
|
||||||
|
let parked = *handle
|
||||||
|
.state::<crate::commands::AppState>()
|
||||||
|
.parked
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
(stage.0, if parked { stage.1 + PARKED_OFFSET } else { stage.1 })
|
||||||
|
}
|
||||||
|
|
||||||
/// Brings one app to the front. Every other app stays live behind it.
|
/// Brings one app to the front. Every other app stays live behind it.
|
||||||
pub fn show_only(
|
pub fn show_only(
|
||||||
handle: &AppHandle,
|
handle: &AppHandle,
|
||||||
@@ -951,9 +964,10 @@ pub fn show_only(
|
|||||||
cfg: &Config,
|
cfg: &Config,
|
||||||
stage: (f64, f64, f64, f64),
|
stage: (f64, f64, f64, f64),
|
||||||
) {
|
) {
|
||||||
|
let (x, y) = placement(handle, stage);
|
||||||
for app in &cfg.apps {
|
for app in &cfg.apps {
|
||||||
let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue };
|
let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue };
|
||||||
let _ = wv.set_position(LogicalPosition::new(stage.0, stage.1));
|
let _ = wv.set_position(LogicalPosition::new(x, y));
|
||||||
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
|
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
|
||||||
let _ = wv.set_zoom(app.zoom);
|
let _ = wv.set_zoom(app.zoom);
|
||||||
let _ = wv.show();
|
let _ = wv.show();
|
||||||
@@ -977,9 +991,10 @@ pub fn set_stage(
|
|||||||
radius: f64,
|
radius: f64,
|
||||||
) {
|
) {
|
||||||
let cfg = handle.state::<crate::commands::AppState>().cfg();
|
let cfg = handle.state::<crate::commands::AppState>().cfg();
|
||||||
|
let (x, y) = placement(handle, stage);
|
||||||
for app in &cfg.apps {
|
for app in &cfg.apps {
|
||||||
let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue };
|
let Some(wv) = handle.get_webview(&label_for(&app.id)) else { continue };
|
||||||
let _ = wv.set_position(LogicalPosition::new(stage.0, stage.1));
|
let _ = wv.set_position(LogicalPosition::new(x, y));
|
||||||
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
|
let _ = wv.set_size(LogicalSize::new(stage.2, stage.3));
|
||||||
set_corner_radius(handle, &app.id, radius);
|
set_corner_radius(handle, &app.id, radius);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -189,12 +189,19 @@ export default function App() {
|
|||||||
/* A native view paints over anything the shell draws, so a dialog needs the
|
/* A native view paints over anything the shell draws, so a dialog needs the
|
||||||
app moved out of the way rather than merely a higher z-index — and once it
|
app moved out of the way rather than merely a higher z-index — and once it
|
||||||
is moved there is nothing left behind the dialog to look at. A still taken
|
is moved there is nothing left behind the dialog to look at. A still taken
|
||||||
on the way out, blurred, puts the background back. */
|
on the way out, blurred, puts the background back.
|
||||||
|
|
||||||
|
This applies to EVERY shell dialog, not just settings. The save-password
|
||||||
|
offer once missed it and rendered behind the app's own view, where it was
|
||||||
|
both invisible and unclickable — so nothing was ever saved, and filling
|
||||||
|
had nothing to recall. */
|
||||||
|
const dialogUp = settingsOpen || offer !== null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
if (settingsOpen) {
|
if (dialogUp) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const shot = await api.stageSnapshot().catch(() => null);
|
const shot = await api.stageSnapshot().catch(() => null);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -208,7 +215,7 @@ export default function App() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [settingsOpen, config]);
|
}, [dialogUp, config]);
|
||||||
|
|
||||||
const toggleCollapse = () => {
|
const toggleCollapse = () => {
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user