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>>,
|
||||
/// Size of the last dialog backdrop still, or why there wasn't one.
|
||||
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 {
|
||||
@@ -105,6 +112,7 @@ pub fn build_state(handle: &AppHandle) -> Result<AppState, String> {
|
||||
last_notification: Mutex::new(String::new()),
|
||||
unread: Mutex::new(std::collections::HashMap::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]
|
||||
pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) {
|
||||
let stage = *state.stage.lock().unwrap();
|
||||
*state.parked.lock().unwrap() = true;
|
||||
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>) {
|
||||
let cfg = state.cfg();
|
||||
let stage = *state.stage.lock().unwrap();
|
||||
*state.parked.lock().unwrap() = false;
|
||||
let active = state.active.lock().unwrap().clone();
|
||||
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.
|
||||
pub fn show_only(
|
||||
handle: &AppHandle,
|
||||
@@ -951,9 +964,10 @@ pub fn show_only(
|
||||
cfg: &Config,
|
||||
stage: (f64, f64, f64, f64),
|
||||
) {
|
||||
let (x, y) = placement(handle, stage);
|
||||
for app in &cfg.apps {
|
||||
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_zoom(app.zoom);
|
||||
let _ = wv.show();
|
||||
@@ -977,9 +991,10 @@ pub fn set_stage(
|
||||
radius: f64,
|
||||
) {
|
||||
let cfg = handle.state::<crate::commands::AppState>().cfg();
|
||||
let (x, y) = placement(handle, stage);
|
||||
for app in &cfg.apps {
|
||||
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));
|
||||
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
|
||||
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
|
||||
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(() => {
|
||||
if (!config) return;
|
||||
let cancelled = false;
|
||||
|
||||
if (settingsOpen) {
|
||||
if (dialogUp) {
|
||||
void (async () => {
|
||||
const shot = await api.stageSnapshot().catch(() => null);
|
||||
if (cancelled) return;
|
||||
@@ -208,7 +215,7 @@ export default function App() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [settingsOpen, config]);
|
||||
}, [dialogUp, config]);
|
||||
|
||||
const toggleCollapse = () => {
|
||||
if (!config) return;
|
||||
|
||||
Reference in New Issue
Block a user