diff --git a/css/style.css b/css/style.css index 488add5..c332c44 100644 --- a/css/style.css +++ b/css/style.css @@ -1258,6 +1258,97 @@ input[type="range"]::-webkit-slider-thumb { display: flex; flex-direction: column; gap: 6px; } +/* ---- Quick search modal (tray menu) ------------------ */ +.quick-search-panel { + padding: 0; + overflow: hidden; + max-width: 520px; + width: 100%; +} +.quick-search-input { + display: flex; align-items: center; gap: 10px; + padding: 14px 18px; + border-bottom: 1px solid var(--border); +} +.quick-search-input svg { + width: 18px; height: 18px; + color: var(--text-dim); + flex-shrink: 0; +} +.quick-search-input input { + flex: 1; + background: transparent; + border: none; + color: var(--text); + font-size: 15px; + outline: none; +} +.quick-search-input kbd { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: var(--bg); + color: var(--text-dim); + border: 1px solid var(--border); +} +.quick-search-results { + max-height: 360px; + overflow-y: auto; + padding: 6px 0; +} +.quick-search-row { + display: flex; align-items: center; gap: 10px; + padding: 8px 18px; + cursor: pointer; +} +.quick-search-row.is-selected { + background: var(--accent-soft); +} +.quick-search-row:hover { background: var(--accent-soft); } +.quick-search-avatar { + width: 32px; height: 32px; + border-radius: var(--radius-sm); + background: var(--bg-elev-2); + display: grid; place-items: center; + overflow: hidden; + font-weight: 600; + font-size: 13px; + flex-shrink: 0; +} +.quick-search-avatar img { + width: 100%; height: 100%; + object-fit: contain; + padding: 4px; + box-sizing: border-box; +} +.quick-search-main { + flex: 1; min-width: 0; +} +.quick-search-name { + font-size: 14px; font-weight: 600; + color: var(--text); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.quick-search-sub { + font-size: 12px; + color: var(--text-dim); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.quick-search-empty { + padding: 24px 18px; + text-align: center; + color: var(--text-dim); + font-size: 13px; + font-style: italic; +} +.quick-search-hint { + border-top: 1px solid var(--border); + padding: 8px 18px; + font-size: 11px; + color: var(--text-faint); + line-height: 1.5; +} + /* Slideover title — tint the "+ New entry" prefix in accent colour so the mode is unambiguous without changing the header layout. */ #slideoverTitle.is-new-mode { color: var(--accent); } diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 659e45b..8668693 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -82,6 +82,10 @@ type FPowerNotify: THandle; // registration handle from PowerRegisterSuspendResumeNotification FSecureClipboard: TSecureClipboard; FBalloonShown: Boolean; + // User setting: gates Shell_NotifyIcon NIF_INFO balloons (currently + // only the "running in the tray" first-time popup, future tray + // notifications would honour the same flag). + FShowNotifications: Boolean; // Window placement captured at MinimizeToTray time. Replayed on // RestoreFromTray so the window comes back in the same state // (maximised / normal + position + size) as before hiding. @@ -91,6 +95,7 @@ type FOnTrayRestore: TProc; FOnLockRequest: TProc; FOnQuit: TProc; + FOnQuickSearchRequest: TProc; // Autofill: global hotkeys → inject credentials into browser. Combos // are user-configurable from Settings; defaults are Ctrl+Shift+L / // Ctrl+Shift+P. We track which IDs are actually live so unregister @@ -157,6 +162,16 @@ type // bridge does not call it itself, so the host stays in control of // shutdown order (server stop, save state, etc.). property OnQuit: TProc read FOnQuit write FOnQuit; + // Fired when the user picks "Quick search…" from the tray menu. + // Handler typically restores the window and pops a JS-side modal + // (Bridge.openQuickSearch) so the user can type to find an entry + // and copy its password without restoring the whole vault UI. + property OnQuickSearchRequest: TProc + read FOnQuickSearchRequest write FOnQuickSearchRequest; + // True (default) = show tray balloon notifications. Set False to keep + // the tray icon mute. Configured from the JS settings panel. + property ShowNotifications: Boolean + read FShowNotifications write FShowNotifications; // Fired on main thread when the autofill hotkey fires. // Args: (ATargetHWND, AWindowTitle). Handler calls ExecuteJavaScript // to let JS match the title against vault entries. @@ -374,6 +389,7 @@ begin FSecureClipboard := TSecureClipboard.Create; FTrayAdded := False; FBalloonShown := False; + FShowNotifications := True; // default on; JS pushes user pref on load // Dedicated message-only window for tray + WTS notifications. FMsgWindow := AllocateHWnd(MsgWindowHandler); @@ -538,8 +554,9 @@ begin ShowWindow(LAppHwnd, SW_HIDE); // 3. First-time only: pop a balloon notification so the user knows the - // app is still running in the tray (and didn't crash). - if not FBalloonShown then + // app is still running in the tray (and didn't crash). Skipped when + // the user opted out via Settings. + if (not FBalloonShown) and FShowNotifications then begin ShowFirstTimeBalloon; FBalloonShown := True; @@ -612,9 +629,10 @@ end; procedure TPMBridge.ShowTrayMenu; const - ID_OPEN = 1; - ID_LOCK = 2; - ID_QUIT = 3; + ID_OPEN = 1; + ID_LOCK = 2; + ID_QUIT = 3; + ID_QUICKSEARCH = 4; var LMenu: HMENU; LPt: TPoint; @@ -624,6 +642,7 @@ begin if LMenu = 0 then Exit; try AppendMenu(LMenu, MF_STRING, ID_OPEN, 'Open'); + AppendMenu(LMenu, MF_STRING, ID_QUICKSEARCH, 'Quick search…'); AppendMenu(LMenu, MF_STRING, ID_LOCK, 'Lock vault'); AppendMenu(LMenu, MF_SEPARATOR, 0, nil); AppendMenu(LMenu, MF_STRING, ID_QUIT, 'Quit'); @@ -642,9 +661,10 @@ begin PostMessage(FMsgWindow, WM_NULL, 0, 0); case LCmd of - ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore(); - ID_LOCK: if Assigned(FOnLockRequest) then FOnLockRequest(); - ID_QUIT: if Assigned(FOnQuit) then FOnQuit(); + ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore(); + ID_QUICKSEARCH: if Assigned(FOnQuickSearchRequest) then FOnQuickSearchRequest(); + ID_LOCK: if Assigned(FOnLockRequest) then FOnLockRequest(); + ID_QUIT: if Assigned(FOnQuit) then FOnQuit(); end; finally DestroyMenu(LMenu); diff --git a/delphi-backend/UMainForm.fmx b/delphi-backend/UMainForm.fmx index 24903f3..ab6677c 100644 --- a/delphi-backend/UMainForm.fmx +++ b/delphi-backend/UMainForm.fmx @@ -4,6 +4,7 @@ object MainForm: TMainForm Caption = 'Password Manager' ClientHeight = 720 ClientWidth = 1100 + WindowState = wsMaximized FormFactor.Width = 320 FormFactor.Height = 480 FormFactor.Devices = [Desktop] diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index d49b143..2d9e83d 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -12,7 +12,8 @@ uses FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser, PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart, - PM.Favicon; + PM.Favicon, + FMX.Platform.Win; // WindowHandleToPlatform → HWND for visibility check type TMainForm = class(TForm) @@ -67,6 +68,7 @@ type ATargetHWND: HWND; const ATitle: string); procedure BridgeDebugHotkey; procedure BridgeNewEntryHotkey(const AWindowTitle: string); + procedure BridgeQuickSearchRequest; procedure WebBrowserInitialized(Sender: TObject); end; @@ -77,6 +79,10 @@ implementation {$R *.fmx} +// Forward — used by WebBrowserInitialized (which sits above the actual +// definition lower in the unit). +function MaskAccessToken(const AUrl: string): string; forward; + procedure TMainForm.FormCreate(Sender: TObject); begin FServer := TPMHTTPServer.Create; @@ -90,6 +96,7 @@ begin FBridge.OnAutofillRequest := BridgeAutofillRequest; FBridge.OnDebugHotkey := BridgeDebugHotkey; FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey; + FBridge.OnQuickSearchRequest := BridgeQuickSearchRequest; FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later FAutofillTargetHWND := 0; @@ -158,6 +165,44 @@ procedure TMainForm.WebBrowserInitialized(Sender: TObject); begin WebBrowser.EnableContextMenu := False; WebBrowser.EnableShowDebugConsole := False; + + // Race-safe navigation fallback: the 1.5 s timer in NavigateToVault + // assumes WebView2 finishes its async init within that window. On slow + // boots / cold-start machines Edge Chromium can take 2-3 s, and the + // timer's Navigate() call lands while the browser is still uninitialised + // → silently dropped → blank window forever. OnInitialized fires once + // the engine is ready, so if a navigation is still pending here, do it + // now. The timer either already ran (FPendingURL == '') or runs later + // and no-ops on the empty string. + FNavTimer.Enabled := False; + if FPendingURL <> '' then + begin + LogLine('OnInitialized fallback nav to: ' + MaskAccessToken(FPendingURL)); + WebBrowser.Navigate(FPendingURL); + FPendingURL := ''; + end; +end; + +procedure TMainForm.BridgeQuickSearchRequest; +var + LWasHidden: Boolean; + LWasHiddenJs: string; +begin + // Tray menu "Quick search…" — bring the window back so the user can + // see the modal, then ask JS to open it. JS handles the locked-vault + // case (shows the auth screen with master-pw focused instead). + if not FServer.Active then Exit; + // Remember whether the window was hidden BEFORE we restore — after the + // user picks an entry the JS layer asks us to minimize back so they can + // paste into the target app without an extra alt-tab. + LWasHidden := (not Self.Visible) or + IsIconic(WindowHandleToPlatform(Self.Handle).Wnd); + FBridge.RestoreFromTray; + LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower; + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&typeof Bridge.openQuickSearch==="function")' + + 'Bridge.openQuickSearch(' + LWasHiddenJs + ')'); + LogLine('Quick search requested from tray menu (wasHidden=' + LWasHiddenJs + ')'); end; procedure TMainForm.BridgeNewEntryHotkey(const AWindowTitle: string); @@ -529,6 +574,22 @@ begin LogLine('App brought to front (autofill picker)'); end + // Used by the quick-search modal: after the user picks an entry the + // password is on the clipboard — if the app was hidden when invoked + // from the tray menu, hide it again so the user can paste straight + // into the target app without alt-tabbing. + else if ACmd = 'app/minimize' then + FBridge.MinimizeToTray + + // Tray balloon notifications on/off. JS pushes the user setting at + // startup (settings_json sync) and whenever they flip the toggle. + else if ACmd = 'tray/notifications' then + begin + FBridge.ShowNotifications := GetParam('enabled') = '1'; + LogLine('Tray notifications ' + + IfThen(FBridge.ShowNotifications, 'enabled', 'disabled')); + end + else if ACmd = 'app/ready' then begin WebBrowser.SetFocus; @@ -735,4 +796,32 @@ begin LogLine('Autofill hotkey (' + LKind + ') — foreground: "' + ATitle + '"'); end; +initialization + // WebView2 ships with a long list of "phone home" behaviours enabled by + // default (SmartScreen lookups, component updates, sync, UMA telemetry, + // domain reliability beacons, optimisation hints, etc.). For a vault + // that's meant to be 100% offline we disable them by passing flags + // through WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS — the standard way to + // configure the embedded Chromium without touching system policy. + // + // Must be set BEFORE the TMS FNC WebBrowser instantiates its + // CoreWebView2Environment, hence the unit initialization block. + SetEnvironmentVariable('WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS', + '--disable-background-networking ' + + '--disable-sync ' + + '--disable-component-update ' + + '--no-default-browser-check ' + + '--no-pings ' + + '--disable-client-side-phishing-detection ' + + '--disable-domain-reliability ' + + '--disable-breakpad ' + + '--disable-crash-reporter ' + + '--no-experiments ' + + '--metrics-recording-only ' + + '--disable-features=MediaRouter,OptimizationHints,InterestFeedContentSuggestions,' + + 'CalculateNativeWinOcclusion,HardwareMediaKeyHandling,Translate,' + + 'NetworkServiceInProcess,BackgroundFetch,SafeBrowsingEnhancedProtection,' + + 'AutofillServerCommunication,PrivacySandboxAdsAPIsOverride' + ); + end. diff --git a/delphi-backend/assets/assets.res b/delphi-backend/assets/assets.res index 3fb97ff..644564f 100644 Binary files a/delphi-backend/assets/assets.res and b/delphi-backend/assets/assets.res differ diff --git a/index.html b/index.html index b390e85..cefef98 100644 --- a/index.html +++ b/index.html @@ -426,6 +426,20 @@ +