feat: chunked file save + export progress spinner + password-reveal + avatar size

Large file save (fixes black screen on big attachment download / export)
- Bridge.saveFile streams anything over ~1MB base64 in chunks through the
  cmd:// channel instead of stuffing the whole payload in one URL — a
  multi-MB base64 URL blew past WebView2's ~2MB navigation cap and blanked
  the document (black screen). Small payloads keep the single-shot path.
- Chunks are sent sequentially (each acked via Bridge.onFileChunkAck
  before the next) so repeated location.href assignments don't coalesce.
- Delphi accumulates chunks per reqId in a TStringBuilder
  (FFileSaveChunks), commits on file/save-commit, and shares the
  decode+dialog+write logic with the single-shot path via SaveDecodedFile.
- Chunk size 1MB → far fewer round-trips (a 20MB export dropped from ~67
  to ~27 hops).

Busy overlay + progress
- Global spinner overlay (showBusy/updateBusy/hideBusy). doExport shows it
  immediately on click — BEFORE the entry-decrypt + attachment-fetch loop
  that is the real cost — with a 0ms yield so it paints before the thread
  blocks (was appearing 3-5s late). Phases: "Reading vault… N/total" →
  "Encrypting export…" → "Preparing file… N%" (real chunk progress).
  Attachment download shows the same for files > 512KB.
- Spinner ring used an undefined --bg-elev-3 (invalid border → invisible);
  switched to --border. Fixed a second stale --bg-elev-3 use on the
  settings-search clear button hover.

Password reveal
- promptDialog gets an eye toggle in password mode, so every encrypted
  prompt (export, import, backup password, recovery code, sync password)
  can show/hide the typed value.

Avatar
- Top-right chip avatar enlarged 22px → 30px with the chip padding
  rebalanced.

Rebuild: BuildAssets + F9 (UMainForm.pas changed for the chunk handlers).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-03 16:56:47 +01:00
parent 3f8ecde571
commit c5fe26ce94
5 changed files with 279 additions and 76 deletions
+34 -7
View File
@@ -741,11 +741,11 @@ input[type="range"]::-webkit-slider-thumb {
.user-menu { position: relative; }
.user-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px;
display: inline-flex; align-items: center; gap: 8px;
padding: 5px 12px 5px 6px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 16px;
border-radius: 20px;
color: var(--text);
font-size: 12px;
transition: all var(--t-fast);
@@ -756,10 +756,10 @@ input[type="range"]::-webkit-slider-thumb {
Falls back to a background-image when a custom picture is set. */
.user-avatar {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px;
margin-left: -4px;
width: 30px; height: 30px;
margin-left: 0;
border-radius: 50%;
font-size: 11px; font-weight: 600;
font-size: 13px; font-weight: 600;
color: #fff;
text-transform: uppercase;
background-size: cover; background-position: center;
@@ -2178,7 +2178,7 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
display: none;
}
.settings-search-clear svg { width: 12px; height: 12px; }
.settings-search-clear:hover { color: var(--text); background: var(--bg-elev-3); }
.settings-search-clear:hover { color: var(--text); background: var(--bg-elev-2); }
.settings-search-wrap.has-query .settings-search-clear { display: inline-flex; }
/* Hidden section + per-row + no-results banner driven by JS. */
.slideover-field.is-search-hidden,
@@ -2191,6 +2191,33 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
display: none;
}
.settings-no-results.is-visible { display: block; }
/* Global busy overlay — spinner + text for long ops (export, big saves). */
.busy-overlay {
position: fixed; inset: 0;
z-index: 200;
display: flex; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
}
.busy-overlay.is-hidden { display: none; }
.busy-box {
display: flex; flex-direction: column; align-items: center; gap: 14px;
padding: 24px 32px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.busy-spinner {
width: 34px; height: 34px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: busy-spin 0.7s linear infinite;
}
@keyframes busy-spin { to { transform: rotate(360deg); } }
.busy-text { font-size: 13px; color: var(--text-dim); text-align: center; }
.slideover-field-label { font-size: 11px; font-weight: 500; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.slideover-field-value {
display: flex; align-items: center; gap: 6px;