feat: Bitwarden import bundle + settings search + quick-search hotkey + UX fixes

- Bitwarden CSV import: folders auto-created server-side; notes column on
  login rows surfaces as a "Notes" custom field instead of polluting tags;
  type=card / type=identity rows now mapped to kind=note with the
  credit-card / identity template + card_* / identity_* columns
  pulled into custom_fields; `fields` column parsed (Bitwarden's
  "label: value\nlabel: value" lines + our own JSON shape).
- Settings panel search: live filter at top of the panel, matches each
  .setting-row individually, hides whole section when no row matches,
  shows a "No matches" banner. Esc clears query (without closing
  Settings); Esc with empty query closes the panel.
- Quick-search hotkey customizable: SetQuickSearchHotkey added to
  PM.Bridge; cmd://autofill/hotkeys extended with qs_mods/qs_vk
  (independent of the autofill enabled flag — quick-search stays
  armed even when autofill is off); state.quickSearchHotkey synced
  via settings_json; new "Quick search picker" row in Settings.
- FireDAC SQLite folder POST/PUT: pre-declare ftString on color/icon
  params so .Clear (NULL) doesn't trip "[FireDAC][Phys][SQLite]-335
  type unknown" at Prepare — was crashing the CSV-import folder
  auto-creation path.
- Edge form-data autocomplete suppressed on slideover inputs (title,
  site, username, password, TOTP, note body, custom fields):
  autocomplete=off (new-password on secrets) + spellcheck=false. Fixes
  the "Informations enregistrées" dropdown popping over data after a
  field was edited.
- closeSlideOver blurs any focused descendant before removing .is-open
  so an invisible focused field can't react to arrow-down / backspace
  after dismissal.
- Slideover Esc handler upgraded to capture phase so it fires before
  the input's own keydown or browser-level Esc swallow on the active
  autocomplete popup.
- Settings panel Esc closes the panel when search input is empty;
  search keeps the keystroke when it has a query to clear.
- Discard-fantome on note open: customFields working copy and
  originalCustomJson now share the SAME normalized array — comparing
  raw plainCustom against the .map()'d working copy made notes look
  dirty on open.
- Delete / Backspace global shortcut: batch-trash on normal views,
  batch perm-delete on trash view, gated on selection + no input
  focused + no modal up.
- Toggle thumb vertical centering via top:50% + translateY(-50%);
  state checked uses translate(16px, -50%) to keep the centring.
- Batch bar disappears after per-card restore/perm-delete/trash:
  state.checked.delete(id) before render for the relevant flows;
  state.checked.clear() before render in emptyTrash and the new
  moveEntriesToFolder helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-06-30 23:25:50 +01:00
parent 6869b7c692
commit bd14831449
7 changed files with 405 additions and 36 deletions
@@ -14,6 +14,7 @@ implementation
uses
System.SysUtils, System.JSON, System.NetEncoding,
System.Generics.Collections,
Data.DB,
FireDAC.Comp.Client, FireDAC.Stan.Param,
IdCustomHTTPServer,
PM.Router, PM.JSON, PM.Database, PM.Session, PM.Audit, PM.RateLimit;
@@ -111,6 +112,10 @@ begin
'VALUES (:uid, :name, :color, :icon)';
LQ.ParamByName('uid').AsInteger := LUserId;
LQ.ParamByName('name').AsString := LName;
// Pre-declare type so .Clear (NULL) doesn't leave the param
// untyped — FireDAC SQLite rejects untyped params at Prepare.
LQ.ParamByName('color').DataType := ftString;
LQ.ParamByName('icon').DataType := ftString;
if LColor = '' then LQ.ParamByName('color').Clear
else LQ.ParamByName('color').AsString := LColor;
if LIcon = '' then LQ.ParamByName('icon').Clear
@@ -205,11 +210,13 @@ begin
LQ.ParamByName('name').AsString := LName;
if LHasColor then
begin
LQ.ParamByName('color').DataType := ftString;
if LColor = '' then LQ.ParamByName('color').Clear
else LQ.ParamByName('color').AsString := LColor;
end;
if LHasIcon then
begin
LQ.ParamByName('icon').DataType := ftString;
if LIcon = '' then LQ.ParamByName('icon').Clear
else LQ.ParamByName('icon').AsString := LIcon;
end;
+16
View File
@@ -146,6 +146,9 @@ type
// stays active.
function SetAutofillHotkeys(AFullMods, AFullVk,
APwdMods, APwdVk: Word): Boolean;
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
// user-chosen combo. True on success.
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
// Convenience wrapper: register the historical defaults (Ctrl+Shift+L
// and Ctrl+Shift+P). Used by the host on first start; runtime changes
// go through SetAutofillHotkeys.
@@ -853,6 +856,19 @@ begin
Result := FAutofillFullActive and FAutofillPwdActive;
end;
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
begin
if FQuickSearchHotkeyRegistered then
begin
UnregisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID);
FQuickSearchHotkeyRegistered := False;
end;
if (AVk <> 0) and (AMods <> 0) then
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
QUICK_SEARCH_HOTKEY_ID, AMods, AVk);
Result := FQuickSearchHotkeyRegistered;
end;
procedure TPMBridge.ApplyTitleBarTheme(ADark: Boolean);
const
DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
+11
View File
@@ -720,6 +720,17 @@ begin
// we register both with the supplied combos (replacing any prior).
else if ACmd = 'autofill/hotkeys' then
begin
// Quick-search combo is set unconditionally (independent of the
// autofill enabled flag) so the picker stays armed even when the
// autofill hotkeys are turned off.
if GetParam('qs_vk') <> '' then
begin
var LQsMods := Word(StrToIntDef(GetParam('qs_mods'), 6));
var LQsVk := Word(StrToIntDef(GetParam('qs_vk'), Ord('Q')));
var LQsOk := FBridge.SetQuickSearchHotkey(LQsMods, LQsVk);
LogLine(Format('Quick-search hotkey set — mods:%d vk:%d (ok=%s)',
[LQsMods, LQsVk, BoolToStr(LQsOk, True)]));
end;
if GetParam('enabled') <> '1' then
begin
FBridge.UnregisterAutofillHotkey;
Binary file not shown.