feat(autofill): Ctrl+Shift+T types the 2FA code
Same workflow as the password-only hotkey: click the 2FA field, press the combo, the current 6-digit code is typed in. Reuses the whole autofill pipeline — no new native typing path. The code is sent in the password slot with an empty username, so Delphi's existing password-only branch types just the code. - Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other two autofill combos; an unconfigured combo (vk=0) doesn't count as a registration failure. - Matching filters on totp_secret BEFORE scoring, so the multi-match picker never offers an entry that has no code. - Configurable + synced like the others (autofillHotkeyTotp), collision-checked against the other four slots, covered by Reset to defaults and the cheatsheet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,12 @@ Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
|
|||||||
|
|
||||||
- `Ctrl+Shift+L` : autofill complet (user + Tab + password)
|
- `Ctrl+Shift+L` : autofill complet (user + Tab + password)
|
||||||
- `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens)
|
- `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens)
|
||||||
|
- `Ctrl+Shift+T` : autofill du **code 2FA** (cliquer le champ 2FA d'abord).
|
||||||
|
ID hotkey 47 / `akTotp`. Ne matche que les entrées avec `totp_secret` (filtre
|
||||||
|
appliqué **avant** le scoring → le picker ne propose jamais un candidat sans
|
||||||
|
TOTP). JS génère le code et l'envoie **dans le slot password avec un username
|
||||||
|
vide** → réutilise le chemin password-only de `ExecuteAutofill`, aucun code
|
||||||
|
natif en plus.
|
||||||
- `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground
|
- `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground
|
||||||
- `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent)
|
- `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ type
|
|||||||
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
|
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
|
||||||
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type
|
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type
|
||||||
// based on this kind.
|
// based on this kind.
|
||||||
TAutofillKind = (akFull, akPasswordOnly);
|
// akTotp reuses the password-only typing path: JS sends the generated
|
||||||
|
// 6-digit code in the password slot with an empty username, so Delphi
|
||||||
|
// types just the code into the focused 2FA field.
|
||||||
|
TAutofillKind = (akFull, akPasswordOnly, akTotp);
|
||||||
|
|
||||||
// Fired when an autofill hotkey is pressed. Args are the foreground
|
// Fired when an autofill hotkey is pressed. Args are the foreground
|
||||||
// window HWND and its title (captured before any focus change), plus
|
// window HWND and its title (captured before any focus change), plus
|
||||||
@@ -117,6 +120,7 @@ type
|
|||||||
FAutofillRegistered: Boolean;
|
FAutofillRegistered: Boolean;
|
||||||
FAutofillFullActive: Boolean;
|
FAutofillFullActive: Boolean;
|
||||||
FAutofillPwdActive: Boolean;
|
FAutofillPwdActive: Boolean;
|
||||||
|
FAutofillTotpActive: Boolean;
|
||||||
FOnAutofillRequest: TAutofillRequestEvent;
|
FOnAutofillRequest: TAutofillRequestEvent;
|
||||||
FDebugHotkeyRegistered: Boolean;
|
FDebugHotkeyRegistered: Boolean;
|
||||||
FOnDebugHotkey: TProc;
|
FOnDebugHotkey: TProc;
|
||||||
@@ -162,8 +166,10 @@ type
|
|||||||
// Returns True if both hotkeys registered successfully. If one or both
|
// Returns True if both hotkeys registered successfully. If one or both
|
||||||
// failed (clash with another app), best-effort: whichever succeeded
|
// failed (clash with another app), best-effort: whichever succeeded
|
||||||
// stays active.
|
// stays active.
|
||||||
|
// ATotpVk = 0 means "no 2FA hotkey configured" — it is then skipped and
|
||||||
|
// does NOT count against the returned all-registered flag.
|
||||||
function SetAutofillHotkeys(AFullMods, AFullVk,
|
function SetAutofillHotkeys(AFullMods, AFullVk,
|
||||||
APwdMods, APwdVk: Word): Boolean;
|
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||||||
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
|
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
|
||||||
// user-chosen combo. True on success.
|
// user-chosen combo. True on success.
|
||||||
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||||||
@@ -287,6 +293,7 @@ const
|
|||||||
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
|
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
|
||||||
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title
|
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title
|
||||||
QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill
|
QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill
|
||||||
|
AUTOFILL_HOTKEY_ID_TOTP = 47; // Ctrl+Shift+T → type the 2FA code
|
||||||
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
|
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
|
||||||
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
|
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
|
||||||
|
|
||||||
@@ -895,13 +902,16 @@ begin
|
|||||||
|
|
||||||
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
|
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
|
||||||
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
|
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
|
||||||
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then
|
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY) or
|
||||||
|
(AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP)) then
|
||||||
begin
|
begin
|
||||||
// Capture the foreground window BEFORE any focus change, then fire the
|
// Capture the foreground window BEFORE any focus change, then fire the
|
||||||
// callback so the host can match the title against vault entries.
|
// callback so the host can match the title against vault entries.
|
||||||
var LKind: TAutofillKind;
|
var LKind: TAutofillKind;
|
||||||
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
|
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
|
||||||
LKind := akPasswordOnly
|
LKind := akPasswordOnly
|
||||||
|
else if AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP then
|
||||||
|
LKind := akTotp
|
||||||
else
|
else
|
||||||
LKind := akFull;
|
LKind := akFull;
|
||||||
var LTarget := GetForegroundWindow;
|
var LTarget := GetForegroundWindow;
|
||||||
@@ -920,7 +930,7 @@ end;
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
|
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
|
||||||
APwdMods, APwdVk: Word): Boolean;
|
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||||||
begin
|
begin
|
||||||
// Tear down whatever is currently registered before installing the new
|
// Tear down whatever is currently registered before installing the new
|
||||||
// combos. RegisterHotKey would fail if the same ID is already taken.
|
// combos. RegisterHotKey would fail if the same ID is already taken.
|
||||||
@@ -934,6 +944,11 @@ begin
|
|||||||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||||||
FAutofillPwdActive := False;
|
FAutofillPwdActive := False;
|
||||||
end;
|
end;
|
||||||
|
if FAutofillTotpActive then
|
||||||
|
begin
|
||||||
|
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
|
||||||
|
FAutofillTotpActive := False;
|
||||||
|
end;
|
||||||
|
|
||||||
// Best-effort registration. A failure (typically MOD_x clash with another
|
// Best-effort registration. A failure (typically MOD_x clash with another
|
||||||
// app's global hotkey) is silent: the other slot can still be live.
|
// app's global hotkey) is silent: the other slot can still be live.
|
||||||
@@ -943,9 +958,15 @@ begin
|
|||||||
if (APwdVk <> 0) and (APwdMods <> 0) then
|
if (APwdVk <> 0) and (APwdMods <> 0) then
|
||||||
FAutofillPwdActive := RegisterHotKey(FMsgWindow,
|
FAutofillPwdActive := RegisterHotKey(FMsgWindow,
|
||||||
AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk);
|
AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk);
|
||||||
|
if (ATotpVk <> 0) and (ATotpMods <> 0) then
|
||||||
|
FAutofillTotpActive := RegisterHotKey(FMsgWindow,
|
||||||
|
AUTOFILL_HOTKEY_ID_TOTP, ATotpMods, ATotpVk);
|
||||||
|
|
||||||
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive;
|
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive
|
||||||
Result := FAutofillFullActive and FAutofillPwdActive;
|
or FAutofillTotpActive;
|
||||||
|
// An unconfigured 2FA combo (vk=0) must not report a registration failure.
|
||||||
|
Result := FAutofillFullActive and FAutofillPwdActive
|
||||||
|
and (FAutofillTotpActive or (ATotpVk = 0));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||||||
@@ -1041,10 +1062,12 @@ end;
|
|||||||
|
|
||||||
procedure TPMBridge.RegisterAutofillHotkey;
|
procedure TPMBridge.RegisterAutofillHotkey;
|
||||||
begin
|
begin
|
||||||
// Convenience default — Ctrl+Shift+L (full) + Ctrl+Shift+P (password).
|
// Convenience defaults — Ctrl+Shift+L (full), Ctrl+Shift+P (password),
|
||||||
|
// Ctrl+Shift+T (2FA code).
|
||||||
// Idempotent: calling twice with the same combos is harmless.
|
// Idempotent: calling twice with the same combos is harmless.
|
||||||
SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'),
|
SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'),
|
||||||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'));
|
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'),
|
||||||
|
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('T'));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPMBridge.UnregisterAutofillHotkey;
|
procedure TPMBridge.UnregisterAutofillHotkey;
|
||||||
@@ -1059,6 +1082,11 @@ begin
|
|||||||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||||||
FAutofillPwdActive := False;
|
FAutofillPwdActive := False;
|
||||||
end;
|
end;
|
||||||
|
if FAutofillTotpActive then
|
||||||
|
begin
|
||||||
|
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
|
||||||
|
FAutofillTotpActive := False;
|
||||||
|
end;
|
||||||
FAutofillRegistered := False;
|
FAutofillRegistered := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -895,10 +895,14 @@ begin
|
|||||||
var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L')));
|
var LFullVk := Word(StrToIntDef(GetParam('full_vk'), Ord('L')));
|
||||||
var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6));
|
var LPwdMods := Word(StrToIntDef(GetParam('pwd_mods'), 6));
|
||||||
var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P')));
|
var LPwdVk := Word(StrToIntDef(GetParam('pwd_vk'), Ord('P')));
|
||||||
|
// 2FA combo — vk 0 when the user cleared it, which skips registration.
|
||||||
|
var LTotpMods := Word(StrToIntDef(GetParam('totp_mods'), 6));
|
||||||
|
var LTotpVk := Word(StrToIntDef(GetParam('totp_vk'), Ord('T')));
|
||||||
var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk,
|
var LAllOk := FBridge.SetAutofillHotkeys(LFullMods, LFullVk,
|
||||||
LPwdMods, LPwdVk);
|
LPwdMods, LPwdVk,
|
||||||
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d (all_ok=%s)',
|
LTotpMods, LTotpVk);
|
||||||
[LFullMods, LFullVk, LPwdMods, LPwdVk, BoolToStr(LAllOk, True)]));
|
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)',
|
||||||
|
[LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)]));
|
||||||
// Notify JS of the result so the UI can flag a failed-to-register combo
|
// Notify JS of the result so the UI can flag a failed-to-register combo
|
||||||
// (typically a clash with another app's global hotkey).
|
// (typically a clash with another app's global hotkey).
|
||||||
WebBrowser.ExecuteJavaScript(
|
WebBrowser.ExecuteJavaScript(
|
||||||
@@ -1542,7 +1546,11 @@ begin
|
|||||||
LTitle := LTitle.Replace('\', '\\');
|
LTitle := LTitle.Replace('\', '\\');
|
||||||
LTitle := LTitle.Replace('"', '\"');
|
LTitle := LTitle.Replace('"', '\"');
|
||||||
|
|
||||||
if AKind = akPasswordOnly then LKind := 'password' else LKind := 'full';
|
case AKind of
|
||||||
|
akPasswordOnly: LKind := 'password';
|
||||||
|
akTotp: LKind := 'totp';
|
||||||
|
else LKind := 'full';
|
||||||
|
end;
|
||||||
|
|
||||||
WebBrowser.ExecuteJavaScript(
|
WebBrowser.ExecuteJavaScript(
|
||||||
'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' +
|
'if(window.Bridge&&typeof Bridge.onAutofillRequest==="function")' +
|
||||||
|
|||||||
@@ -675,6 +675,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button>
|
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillPwdCombo">Ctrl+Shift+P</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span style="font-size:12px;color:var(--text-dim)">
|
||||||
|
2FA code only (click the 2FA field first)
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingAutofillTotpCombo">Ctrl+Shift+T</button>
|
||||||
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<span style="font-size:12px;color:var(--text-dim)">
|
<span style="font-size:12px;color:var(--text-dim)">
|
||||||
Quick search picker (find entry, fill from anywhere)
|
Quick search picker (find entry, fill from anywhere)
|
||||||
|
|||||||
+43
-4
@@ -81,12 +81,14 @@ function autofillCaptureFromEvent(e) {
|
|||||||
function autofillPushHotkeys() {
|
function autofillPushHotkeys() {
|
||||||
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
|
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
|
||||||
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
|
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
|
||||||
|
localStorage.setItem('autofillHotkeyTotp', JSON.stringify(state.autofillHotkeyTotp));
|
||||||
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
|
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
|
||||||
localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey));
|
localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey));
|
||||||
if (Bridge.active) {
|
if (Bridge.active) {
|
||||||
Bridge.setAutofillHotkeys(state.autofillEnabled, {
|
Bridge.setAutofillHotkeys(state.autofillEnabled, {
|
||||||
full: state.autofillHotkeyFull,
|
full: state.autofillHotkeyFull,
|
||||||
password: state.autofillHotkeyPwd,
|
password: state.autofillHotkeyPwd,
|
||||||
|
totp: state.autofillHotkeyTotp,
|
||||||
quickSearch: state.quickSearchHotkey,
|
quickSearch: state.quickSearchHotkey,
|
||||||
newEntry: state.newEntryHotkey,
|
newEntry: state.newEntryHotkey,
|
||||||
});
|
});
|
||||||
@@ -209,13 +211,22 @@ async function autofillHandleRequest(windowTitle, kind) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const titleLower = autofillStripBrowserSuffix(windowTitle).toLowerCase();
|
const titleLower = autofillStripBrowserSuffix(windowTitle).toLowerCase();
|
||||||
const scored = state.entries
|
// The 2FA hotkey can only serve entries that actually carry a TOTP secret
|
||||||
|
// — filtering here (not after scoring) also means the picker never lists a
|
||||||
|
// candidate that would fail once chosen.
|
||||||
|
const pool = (kind === 'totp')
|
||||||
|
? state.entries.filter(e => e.totp_secret && e.totp_iv)
|
||||||
|
: state.entries;
|
||||||
|
const scored = pool
|
||||||
.map(e => ({ entry: e, score: autofillScore(e, titleLower) }))
|
.map(e => ({ entry: e, score: autofillScore(e, titleLower) }))
|
||||||
.filter(x => x.score > 0)
|
.filter(x => x.score > 0)
|
||||||
.sort((a, b) => b.score - a.score);
|
.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
if (scored.length === 0) {
|
if (scored.length === 0) {
|
||||||
toast('Autofill: no match for "' + windowTitle.slice(0, 40) + '"', 'warning');
|
toast(kind === 'totp'
|
||||||
|
? 'No 2FA code stored for "' + windowTitle.slice(0, 40) + '"'
|
||||||
|
: 'Autofill: no match for "' + windowTitle.slice(0, 40) + '"',
|
||||||
|
'warning');
|
||||||
Bridge.cancelAutofill();
|
Bridge.cancelAutofill();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -230,8 +241,34 @@ async function autofillHandleRequest(windowTitle, kind) {
|
|||||||
openAutofillPicker(scored.map(x => x.entry), windowTitle, kind);
|
openAutofillPicker(scored.map(x => x.entry), windowTitle, kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt and type an entry. kind = 'full' or 'password'.
|
// Decrypt and type an entry. kind = 'full' | 'password' | 'totp'.
|
||||||
async function autofillFillEntry(entry, kind) {
|
async function autofillFillEntry(entry, kind) {
|
||||||
|
// 2FA: type the current 6-digit code instead of the password. Sent in the
|
||||||
|
// password slot with an empty username, so Delphi's password-only path
|
||||||
|
// types it as-is — no new native code path.
|
||||||
|
if (kind === 'totp') {
|
||||||
|
const secret = await decryptTotpSecret(entry.totp_secret, entry.totp_iv);
|
||||||
|
if (!secret || secret === '[ERROR]') {
|
||||||
|
toast('2FA: cannot decrypt the secret', 'error');
|
||||||
|
Bridge.cancelAutofill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let code;
|
||||||
|
try { code = (await generateTOTP(secret)).code; }
|
||||||
|
catch (e) {
|
||||||
|
toast('2FA: invalid secret for ' + entryDisplayName(entry), 'error');
|
||||||
|
Bridge.cancelAutofill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
autofillPendingToast = '2FA code filled: ' + entryDisplayName(entry);
|
||||||
|
Bridge.executeAutofill('', code);
|
||||||
|
fetch('' + '/audit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
body: JSON.stringify({ action: 'autofill_totp', site: entry.site }),
|
||||||
|
}).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const password = await decryptPwd(entry.encrypted_password, entry.iv);
|
const password = await decryptPwd(entry.encrypted_password, entry.iv);
|
||||||
if (password === '[ERROR]') {
|
if (password === '[ERROR]') {
|
||||||
toast('Autofill: decryption error', 'error');
|
toast('Autofill: decryption error', 'error');
|
||||||
@@ -285,7 +322,9 @@ function openAutofillPicker(entries, windowTitle, kind) {
|
|||||||
}
|
}
|
||||||
list.appendChild(btn);
|
list.appendChild(btn);
|
||||||
});
|
});
|
||||||
const head = (kind === 'password' ? 'Pick entry (password only) — ' : 'Pick entry — ')
|
const head = (kind === 'totp' ? 'Pick entry (2FA code) — '
|
||||||
|
: kind === 'password' ? 'Pick entry (password only) — '
|
||||||
|
: 'Pick entry — ')
|
||||||
+ entries.length + ' match "' + windowTitle.slice(0, 30) + '…"';
|
+ entries.length + ' match "' + windowTitle.slice(0, 30) + '…"';
|
||||||
$('#autofillPickerTitle').textContent = head;
|
$('#autofillPickerTitle').textContent = head;
|
||||||
$('#autofillPickerModal').classList.remove('is-hidden');
|
$('#autofillPickerModal').classList.remove('is-hidden');
|
||||||
|
|||||||
@@ -205,9 +205,14 @@ const Bridge = (() => {
|
|||||||
const n = autofillComboToWin32(combos.newEntry);
|
const n = autofillComboToWin32(combos.newEntry);
|
||||||
ne = '&ne_mods=' + n.mods + '&ne_vk=' + n.vk;
|
ne = '&ne_mods=' + n.mods + '&ne_vk=' + n.vk;
|
||||||
}
|
}
|
||||||
|
let tp = '';
|
||||||
|
if (combos.totp) {
|
||||||
|
const t = autofillComboToWin32(combos.totp);
|
||||||
|
tp = '&totp_mods=' + t.mods + '&totp_vk=' + t.vk;
|
||||||
|
}
|
||||||
cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') +
|
cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') +
|
||||||
'&full_mods=' + f.mods + '&full_vk=' + f.vk +
|
'&full_mods=' + f.mods + '&full_vk=' + f.vk +
|
||||||
'&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne);
|
'&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne + tp);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Called by Delphi after a setAutofillHotkeys request, with true if
|
// Called by Delphi after a setAutofillHotkeys request, with true if
|
||||||
@@ -673,6 +678,8 @@ const state = {
|
|||||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'),
|
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"L"}'),
|
||||||
autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') ||
|
autofillHotkeyPwd: JSON.parse(localStorage.getItem('autofillHotkeyPwd') ||
|
||||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'),
|
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'),
|
||||||
|
autofillHotkeyTotp: JSON.parse(localStorage.getItem('autofillHotkeyTotp') ||
|
||||||
|
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"T"}'),
|
||||||
quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') ||
|
quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') ||
|
||||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'),
|
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'),
|
||||||
newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') ||
|
newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') ||
|
||||||
@@ -6302,6 +6309,7 @@ function openSettings() {
|
|||||||
// Hotkey capture buttons — labels reflect current combos.
|
// Hotkey capture buttons — labels reflect current combos.
|
||||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||||
|
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
|
||||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||||
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||||
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
||||||
@@ -6690,7 +6698,8 @@ const SYNCED_SETTING_KEYS = [
|
|||||||
// Hotkey combos are user preferences — values are portable. The
|
// Hotkey combos are user preferences — values are portable. The
|
||||||
// registration itself is Windows-only, so non-Windows clients just
|
// registration itself is Windows-only, so non-Windows clients just
|
||||||
// ignore them.
|
// ignore them.
|
||||||
'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', 'newEntryHotkey',
|
'autofillHotkeyFull', 'autofillHotkeyPwd', 'autofillHotkeyTotp',
|
||||||
|
'quickSearchHotkey', 'newEntryHotkey',
|
||||||
// Sidebar section collapsed state. Object of { folders, tags, tools }
|
// Sidebar section collapsed state. Object of { folders, tags, tools }
|
||||||
// booleans. Synced so the user gets the same fold state across devices.
|
// booleans. Synced so the user gets the same fold state across devices.
|
||||||
'sidebarCollapsed',
|
'sidebarCollapsed',
|
||||||
@@ -6764,6 +6773,7 @@ async function loadServerSettings() {
|
|||||||
case 'pageSize': localStorage.setItem('pageSize', String(v)); break;
|
case 'pageSize': localStorage.setItem('pageSize', String(v)); break;
|
||||||
case 'autofillHotkeyFull':
|
case 'autofillHotkeyFull':
|
||||||
case 'autofillHotkeyPwd':
|
case 'autofillHotkeyPwd':
|
||||||
|
case 'autofillHotkeyTotp':
|
||||||
case 'quickSearchHotkey':
|
case 'quickSearchHotkey':
|
||||||
case 'newEntryHotkey':
|
case 'newEntryHotkey':
|
||||||
case 'sidebarCollapsed':
|
case 'sidebarCollapsed':
|
||||||
@@ -7702,6 +7712,7 @@ async function init() {
|
|||||||
const others = [
|
const others = [
|
||||||
kind !== 'full' ? state.autofillHotkeyFull : null,
|
kind !== 'full' ? state.autofillHotkeyFull : null,
|
||||||
kind !== 'password' ? state.autofillHotkeyPwd : null,
|
kind !== 'password' ? state.autofillHotkeyPwd : null,
|
||||||
|
kind !== 'totp' ? state.autofillHotkeyTotp : null,
|
||||||
kind !== 'quickSearch' ? state.quickSearchHotkey : null,
|
kind !== 'quickSearch' ? state.quickSearchHotkey : null,
|
||||||
kind !== 'newEntry' ? state.newEntryHotkey : null,
|
kind !== 'newEntry' ? state.newEntryHotkey : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
@@ -7714,6 +7725,7 @@ async function init() {
|
|||||||
// Commit.
|
// Commit.
|
||||||
if (kind === 'full') state.autofillHotkeyFull = captured;
|
if (kind === 'full') state.autofillHotkeyFull = captured;
|
||||||
else if (kind === 'password') state.autofillHotkeyPwd = captured;
|
else if (kind === 'password') state.autofillHotkeyPwd = captured;
|
||||||
|
else if (kind === 'totp') state.autofillHotkeyTotp = captured;
|
||||||
else if (kind === 'quickSearch') state.quickSearchHotkey = captured;
|
else if (kind === 'quickSearch') state.quickSearchHotkey = captured;
|
||||||
else /* newEntry */ state.newEntryHotkey = captured;
|
else /* newEntry */ state.newEntryHotkey = captured;
|
||||||
btn.textContent = autofillComboLabel(captured);
|
btn.textContent = autofillComboLabel(captured);
|
||||||
@@ -7726,16 +7738,19 @@ async function init() {
|
|||||||
}
|
}
|
||||||
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
||||||
bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
|
bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
|
||||||
|
bindHotkeyCapture('#settingAutofillTotpCombo', 'totp');
|
||||||
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
|
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
|
||||||
bindHotkeyCapture('#settingNewEntryCombo', 'newEntry');
|
bindHotkeyCapture('#settingNewEntryCombo', 'newEntry');
|
||||||
|
|
||||||
$('#settingAutofillResetHotkeys').addEventListener('click', () => {
|
$('#settingAutofillResetHotkeys').addEventListener('click', () => {
|
||||||
state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' };
|
state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' };
|
||||||
state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' };
|
state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' };
|
||||||
|
state.autofillHotkeyTotp = { ctrl: true, shift: true, alt: false, win: false, key: 'T' };
|
||||||
state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' };
|
state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' };
|
||||||
state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' };
|
state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' };
|
||||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||||
|
$('#settingAutofillTotpCombo').textContent = autofillComboLabel(state.autofillHotkeyTotp);
|
||||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||||
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||||
autofillPushHotkeys();
|
autofillPushHotkeys();
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ const CHEATSHEET_GROUPS = [
|
|||||||
items: [
|
items: [
|
||||||
{ keys: ['Ctrl', 'Shift', 'L'], desc: 'Autofill username + password into the active window' },
|
{ keys: ['Ctrl', 'Shift', 'L'], desc: 'Autofill username + password into the active window' },
|
||||||
{ keys: ['Ctrl', 'Shift', 'P'], desc: 'Autofill password only (step-2 forms, unlock screens)' },
|
{ keys: ['Ctrl', 'Shift', 'P'], desc: 'Autofill password only (step-2 forms, unlock screens)' },
|
||||||
|
{ keys: ['Ctrl', 'Shift', 'T'], desc: 'Autofill the 2FA code (click the 2FA field first)' },
|
||||||
{ keys: ['Ctrl', 'Shift', 'Q'], desc: 'Quick search → SendInput password into the active window' },
|
{ keys: ['Ctrl', 'Shift', 'Q'], desc: 'Quick search → SendInput password into the active window' },
|
||||||
{ keys: ['Ctrl', 'Shift', 'A'], desc: 'Quick-add a new entry pre-filled with the foreground window title' },
|
{ keys: ['Ctrl', 'Shift', 'A'], desc: 'Quick-add a new entry pre-filled with the foreground window title' },
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user