feat: quick-search fill modes + editable custom-field combobox + JS build gate
Quick search (Ctrl+Shift+Q fill mode) - Enter / left-click → full autofill (username + Tab + password), like Ctrl+Shift+L. - Shift+Enter / right-click → username only (new Delphi username-only SendInput path via field=user; ExecuteAutofill AUsernameOnly param). - Ctrl+Enter / Ctrl+click → password only. - Copy mode (tray / palette) unchanged: Enter/left = password, Shift+Enter/right = username. - Clipboard fix: copy-then-minimise no longer wipes the just-copied password — MinimizeToTray takes an AClearClipboard flag (False on the quick-search copy path, driven by app/minimize?keepclip=1). The 30s auto-clear still guards it. - Right-click on a result row suppresses the native/custom context menu (preventDefault + stopPropagation). Editable custom-field combobox - Option-backed custom fields (card brand, expiry year/month, etc.) now render a custom editable combobox instead of a locked <select>: an arrow drops a menu of ALL options (a native <datalist> filtered to the typed text, which confused users), while the input stays freely typeable for values not in the list. Storage shape unchanged. - Outside-click closes the menu via the existing slideover mousedown handler; item mousedown + preventDefault so blur doesn't race the pick. Build safety - BuildAssets.ps1 runs `node --check` on every embedded .js before generating assets.res. A syntax error now aborts the asset build (exit 1, file + line logged) instead of shipping a dead bundle that only surfaces after a full Delphi rebuild. Node is optional: absent → warn and continue. Docs - CODE_AUDIT.md: full static-analysis report (security, latent bugs, maintainability, future features, prioritized action plan). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -129,8 +129,11 @@ type
|
||||
public
|
||||
constructor Create(AMainForm: TForm);
|
||||
destructor Destroy; override;
|
||||
// Hide main window and show tray icon.
|
||||
procedure MinimizeToTray;
|
||||
// Hide main window and show tray icon. AClearClipboard defaults to
|
||||
// True (a manual minimise wipes any copied password immediately);
|
||||
// the quick-search "copy then hide so I can paste" flow passes False
|
||||
// so it doesn't nuke the password it just placed on the clipboard.
|
||||
procedure MinimizeToTray(AClearClipboard: Boolean = True);
|
||||
// Restore main window and remove tray icon.
|
||||
procedure RestoreFromTray;
|
||||
// Apply Windows dark-mode title bar to the main form. Win10 19044+
|
||||
@@ -160,8 +163,10 @@ type
|
||||
// If AUsername is empty, only the password is typed (no Tab) — matches
|
||||
// the password-only hotkey path AND avoids spurious Tab on entries
|
||||
// without a stored username.
|
||||
// AUsernameOnly = True → type ONLY the username (no Tab, no password);
|
||||
// used by the quick-search "autofill username" action.
|
||||
procedure ExecuteAutofill(ATargetHWND: HWND;
|
||||
const AUsername, APassword: string);
|
||||
const AUsername, APassword: string; AUsernameOnly: Boolean = False);
|
||||
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
||||
property TrayAdded: Boolean read FTrayAdded;
|
||||
property AutofillRegistered: Boolean read FAutofillRegistered;
|
||||
@@ -551,7 +556,7 @@ begin
|
||||
until LWnd = 0;
|
||||
end;
|
||||
|
||||
procedure TPMBridge.MinimizeToTray;
|
||||
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True);
|
||||
var
|
||||
LFormHwnd, LAppHwnd: HWND;
|
||||
begin
|
||||
@@ -561,8 +566,12 @@ begin
|
||||
// Extra safety: clear the clipboard immediately when the user minimizes,
|
||||
// rather than waiting for the 30s auto-clear timer to fire. A password
|
||||
// the user just copied shouldn't sit in the clipboard while the app is
|
||||
// out of sight.
|
||||
FSecureClipboard.Clear;
|
||||
// out of sight. SKIPPED when AClearClipboard=False — the quick-search
|
||||
// copy-then-hide flow deliberately keeps the password on the clipboard
|
||||
// (the 30s auto-clear timer still guards it) so the user can paste it
|
||||
// into their target app after we minimise.
|
||||
if AClearClipboard then
|
||||
FSecureClipboard.Clear;
|
||||
|
||||
LFormHwnd := MainFormHWND(FMainForm);
|
||||
LAppHwnd := FindFMXAppWindow;
|
||||
@@ -1115,7 +1124,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
|
||||
const AUsername, APassword: string);
|
||||
const AUsername, APassword: string; AUsernameOnly: Boolean = False);
|
||||
const
|
||||
MinimizeSettleMs = 80;
|
||||
FocusSettleDelayMs = 120;
|
||||
@@ -1135,6 +1144,16 @@ begin
|
||||
WaitForModifierRelease(1000);
|
||||
Sleep(FocusSettleDelayMs);
|
||||
|
||||
// Username-only: type just the username into the focused field, no Tab,
|
||||
// no password. Used by the quick-search right-click / Shift+Enter path.
|
||||
if AUsernameOnly then
|
||||
begin
|
||||
SendSelectAllAndDelete;
|
||||
Sleep(60);
|
||||
SendUnicodeString(AUsername);
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if AUsername = '' then
|
||||
begin
|
||||
SendSelectAllAndDelete;
|
||||
|
||||
@@ -99,6 +99,9 @@ type
|
||||
// SendInput because Win10/11 anti-focus-stealing rules then refuse to
|
||||
// hand focus to the target window.
|
||||
FAutofillPendingHide: Boolean;
|
||||
// True when the pending autofill should type ONLY the username (quick
|
||||
// search "autofill username" — right-click / Shift+Enter in fill mode).
|
||||
FAutofillPendingUserOnly: Boolean;
|
||||
// Created dynamically in FormCreate so the directive can pick either
|
||||
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
|
||||
// needing two .fmx variants. Aligned to Client to fill the remaining
|
||||
@@ -788,6 +791,7 @@ begin
|
||||
FAutofillPendingPass := GetParam('password');
|
||||
FAutofillPendingHWND := FAutofillTargetHWND;
|
||||
FAutofillPendingHide := GetParam('hide_after') = '1';
|
||||
FAutofillPendingUserOnly := GetParam('field') = 'user';
|
||||
FAutofillTargetHWND := 0;
|
||||
|
||||
// Small timer so SetForegroundWindow has time to take effect before
|
||||
@@ -823,7 +827,9 @@ begin
|
||||
// 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
|
||||
// keepclip=1 → don't wipe the clipboard on minimise (quick-search
|
||||
// copy-then-hide flow). Default clears it as before.
|
||||
FBridge.MinimizeToTray(GetParam('keepclip') <> '1')
|
||||
|
||||
// Tray balloon notifications on/off. JS pushes the user setting at
|
||||
// startup (settings_json sync) and whenever they flip the toggle.
|
||||
@@ -1323,22 +1329,24 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
|
||||
var
|
||||
TargetHwnd: HWND;
|
||||
PendingUser, PendingPass: string;
|
||||
HideAfter: Boolean;
|
||||
HideAfter, UserOnly: Boolean;
|
||||
ForegroundAfter: HWND;
|
||||
begin
|
||||
TargetHwnd := FAutofillPendingHWND;
|
||||
PendingUser := FAutofillPendingUser;
|
||||
PendingPass := FAutofillPendingPass;
|
||||
HideAfter := FAutofillPendingHide;
|
||||
UserOnly := FAutofillPendingUserOnly;
|
||||
FAutofillPendingHWND := 0;
|
||||
FAutofillPendingUser := '';
|
||||
FAutofillPendingPass := '';
|
||||
FAutofillPendingHide := False;
|
||||
FAutofillPendingUserOnly := False;
|
||||
|
||||
TTimer(Sender).Enabled := False;
|
||||
TTimer(Sender).Free;
|
||||
|
||||
FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass);
|
||||
FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass, UserOnly);
|
||||
ForegroundAfter := GetForegroundWindow;
|
||||
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s',
|
||||
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
|
||||
|
||||
@@ -77,6 +77,33 @@ if ($files.Count -eq 0) {
|
||||
Log "Embedding $($files.Count) file(s):"
|
||||
$files | ForEach-Object { Log (" " + $_.UrlPath + " -> " + $_.ResName) }
|
||||
|
||||
# --- JS syntax gate -----------------------------------------------------------
|
||||
# A syntax error in app.js parses fine here but kills the whole frontend at
|
||||
# runtime (no event handlers → dead UI), and it's only caught after a full
|
||||
# Delphi rebuild. Run `node --check` on every embedded .js so a broken bundle
|
||||
# never makes it into assets.res. Node is optional: if it isn't installed we
|
||||
# warn and continue rather than blocking the build on a machine without it.
|
||||
$node = Get-Command node.exe -ErrorAction SilentlyContinue
|
||||
if (-not $node) { $node = Get-Command node -ErrorAction SilentlyContinue }
|
||||
$jsFiles = $files | Where-Object { $_.Relative -match '\.js$' }
|
||||
if ($jsFiles) {
|
||||
if ($node) {
|
||||
foreach ($jf in $jsFiles) {
|
||||
Log "Syntax check: $($jf.Relative)"
|
||||
# --check prints errors to stderr and returns non-zero on failure.
|
||||
$out = & $node.Source --check $jf.FullPath 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Log "JS SYNTAX ERROR in $($jf.Relative):"
|
||||
Log ($out | Out-String)
|
||||
throw "JS syntax check failed for $($jf.Relative) - aborting asset build."
|
||||
}
|
||||
}
|
||||
Log "JS syntax OK."
|
||||
} else {
|
||||
Log "WARNING: node not found - skipping JS syntax check. Install Node to enable it."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Generate assets.rc -------------------------------------------------------
|
||||
$rc = New-Object System.Text.StringBuilder
|
||||
[void]$rc.AppendLine('// Auto-generated by BuildAssets.ps1 - do not edit by hand.')
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user