feat: MFA tools, single-instance, tray polish, prefs persistence
Session highlights:
- feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes
rememberedUsername being lost across reboots due to the random
ephemeral HTTP port changing the localStorage origin every launch.
Bridge cmd://prefs/{get,set} round-trips through Delphi.
- feat(tray): icon visible from startup (NIM_ADD at constructor, not
at first minimize). Tray context menu themed via uxtheme!135
SetPreferredAppMode so it follows the app's dark/light setting.
- feat(single-instance): named mutex + RegisterWindowMessage broadcast.
Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the
running bridge restores the window from tray. Mutex lives in Local\
namespace so distinct Windows users can still each run one.
- feat(mfa): Authenticator sidebar view (live TOTP codes for every
entry with a secret) + standalone TOTP generator modal (paste
base32 / otpauth:// URI, or generate a random 20-byte secret).
- feat(sidebar): Folders / Tags / Tools sections collapsible with
chevron toggle. Badge counts stay visible when collapsed. State
persisted in settings_json (synced across devices).
- feat(autofill): hotkey when vault is locked now restores the app
and focuses the master password input instead of no-op'ing
silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the
vault was locked" path.
- feat(quick-unlock): when enabled, skip lockVault on Windows lock /
sleep. Rationale: the DPAPI blob already gates access via the
Windows account, so re-locking on top of the OS lock is redundant.
Idle auto-lock still fires (separate opt-in).
- fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI
source-of-truth at boot, instead of trusting (now-volatile)
localStorage.
- docs: CLAUDE.md updated with all new modules, bridge commands,
and the port-ephemeral pitfall.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+428
-19
@@ -490,6 +490,41 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
.section-toggle {
|
||||
flex: 1;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
background: none; border: none; padding: 2px 0;
|
||||
font: inherit; color: inherit; text-transform: inherit;
|
||||
letter-spacing: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.section-toggle:hover { color: var(--text-dim); }
|
||||
.section-count {
|
||||
min-width: 16px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 9px;
|
||||
background: var(--bg);
|
||||
color: var(--text-dim);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
/* Push everything after the count toward the right of the toggle button
|
||||
so the +/etc. action button stays at the row's edge. */
|
||||
.section-toggle { gap: 6px; }
|
||||
.section-chevron {
|
||||
width: 12px; height: 12px;
|
||||
transition: transform 0.18s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-section.is-collapsed .section-chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.sidebar-section.is-collapsed .section-body {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-bottom {
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
@@ -513,6 +548,13 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: var(--blur);
|
||||
-webkit-backdrop-filter: var(--blur);
|
||||
position: relative;
|
||||
/* backdrop-filter creates a stacking context, so child z-indexes (e.g.
|
||||
.user-dropdown : 50) are confined here. Without an explicit z-index
|
||||
the topbar lands behind .slideover (z-index 30), clipping the
|
||||
dropdown when an entry detail panel is open. 40 puts the topbar
|
||||
(and its children) above the slideover; modals at 200+ still win. */
|
||||
z-index: 40;
|
||||
}
|
||||
.search { flex: 1; max-width: 480px; position: relative; display: flex; align-items: center; }
|
||||
.search svg {
|
||||
@@ -615,14 +657,6 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
}
|
||||
|
||||
/* Marquee rectangle for rubber-band selection */
|
||||
.marquee {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
position: relative;
|
||||
@@ -714,26 +748,55 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
restore + purge (trash view), or kebab (compact mode). Same order slot. */
|
||||
.entry-grid.is-list .entry-fav,
|
||||
.entry-grid.is-list .entry-del,
|
||||
.entry-grid.is-list .entry-dup,
|
||||
.entry-grid.is-list .entry-kebab-wrap,
|
||||
.entry-grid.is-list .entry-head .icon-btn {
|
||||
order: 6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.entry-avatar.is-checkable {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.entry-avatar.is-checkable:hover::after {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
/* Selection checkbox overlay — nested INSIDE the avatar (.entry-avatar
|
||||
is position:relative) so it covers the avatar 1:1, no overlap with the
|
||||
avatar's footprint. Hidden by default; appears on card hover OR when
|
||||
the entry is already checked. The avatar's initials stay visible
|
||||
underneath through the checkbox's transparent-then-solid transition. */
|
||||
.entry-check {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid; place-items: center;
|
||||
background: var(--bg);
|
||||
border: 1.5px solid var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent-soft);
|
||||
border: 2px solid var(--accent);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--t-fast),
|
||||
background var(--t-fast), border-color var(--t-fast);
|
||||
z-index: 2;
|
||||
}
|
||||
.entry-card.is-checked .entry-avatar {
|
||||
.entry-card:hover .entry-check,
|
||||
.entry-check.is-checked {
|
||||
opacity: 1;
|
||||
}
|
||||
.entry-check.is-checked {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
/* Standalone variant used in the table view's check column. The default
|
||||
rule above is absolute+inset:0 so the box sizes itself from its avatar
|
||||
parent (card view). When the checkbox lives on its own in a <td>, no
|
||||
parent provides a size and the box collapses to a thin line. This
|
||||
modifier restores explicit dimensions and always-on visibility. */
|
||||
.entry-check.entry-check-static {
|
||||
position: static;
|
||||
width: 18px; height: 18px;
|
||||
border-radius: 4px;
|
||||
border-width: 1.5px;
|
||||
opacity: 1;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Batch action bar (appears when selection is non-empty) */
|
||||
.batch-bar {
|
||||
@@ -766,6 +829,7 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
|
||||
.entry-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
||||
.entry-avatar {
|
||||
position: relative; /* anchor for .entry-check overlay */
|
||||
width: 32px; height: 32px;
|
||||
display: grid; place-items: center;
|
||||
background: var(--bg-elev-2);
|
||||
@@ -784,7 +848,7 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
font-size: 12px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.entry-fav, .entry-del {
|
||||
.entry-fav, .entry-del, .entry-dup {
|
||||
color: var(--text-faint);
|
||||
background: transparent; border: none;
|
||||
padding: 4px;
|
||||
@@ -793,6 +857,18 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
}
|
||||
.entry-fav.is-on { color: var(--warning); }
|
||||
.entry-fav:hover { color: var(--warning); transform: scale(1.1); }
|
||||
.entry-dup {
|
||||
color: var(--text-dim);
|
||||
opacity: 0;
|
||||
}
|
||||
.entry-card:hover .entry-dup { opacity: 0.8; }
|
||||
.entry-dup:hover {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: 6px;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Delete button — always visible, brightens on hover */
|
||||
.entry-del {
|
||||
@@ -862,6 +938,10 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
animation: modalIn var(--t-fast);
|
||||
}
|
||||
.entry-kebab-menu.is-open { display: block; }
|
||||
/* Lift the card containing an open kebab menu above its siblings so the
|
||||
dropdown isn't clipped by the next card in DOM order. */
|
||||
.entry-card:has(.entry-kebab-menu.is-open) { z-index: 100; }
|
||||
.entry-row:has(.entry-kebab-menu.is-open) { position: relative; z-index: 100; }
|
||||
|
||||
.kebab-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
@@ -966,6 +1046,62 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
transition: width 0.8s linear, background 0.2s;
|
||||
}
|
||||
|
||||
/* ---- Authenticator view (TOTP grid) ---------------------- */
|
||||
.entry-grid.is-auth {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.auth-card {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, transform 0.05s;
|
||||
}
|
||||
.auth-card:hover { border-color: var(--accent); }
|
||||
.auth-card:active { transform: scale(0.99); }
|
||||
.auth-card-head { margin-bottom: 8px; }
|
||||
.auth-card-title {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.auth-card-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.auth-card-code-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.auth-card-code {
|
||||
flex: 1;
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-size: 26px;
|
||||
letter-spacing: 3px;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
user-select: all;
|
||||
}
|
||||
.auth-card-bar-wrap {
|
||||
margin-top: 8px;
|
||||
height: 3px;
|
||||
background: var(--bg);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auth-card-bar {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 100%;
|
||||
transition: width 0.8s linear, background 0.2s;
|
||||
}
|
||||
.auth-card-bar.is-warning { background: #f59e0b; }
|
||||
|
||||
/* ---- 10. SLIDE-OVER -------------------------------------- */
|
||||
|
||||
.slideover {
|
||||
@@ -1276,3 +1412,276 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
.content { padding: 16px; }
|
||||
.slideover { width: 100%; }
|
||||
}
|
||||
|
||||
/* ---- 17. AUTOFILL PICKER -------------------------------- */
|
||||
|
||||
.autofill-pick-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
.autofill-pick-btn:hover { background: var(--bg-elev-2); }
|
||||
.autofill-pick-site {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
.autofill-pick-user {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ---- Hotkey capture buttons (Settings → Autofill) ---- */
|
||||
.hotkey-capture-btn {
|
||||
font-family: ui-monospace, "Cascadia Code", Menlo, monospace;
|
||||
font-size: 11px;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
}
|
||||
.hotkey-capture-btn.is-capturing {
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg, #fff);
|
||||
animation: pulse-capture 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-capture {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* ---- Card display title + subtitle (when entry.title set) ---- */
|
||||
.entry-subtitle {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 1px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---- Sidebar "(no folder)" pseudo-entry ---- */
|
||||
.nav-item.is-uncategorized {
|
||||
color: var(--text-faint);
|
||||
font-style: italic;
|
||||
}
|
||||
.nav-item.is-uncategorized:hover { color: var(--text-dim); }
|
||||
.nav-item.is-uncategorized.is-active { color: var(--text); font-style: normal; }
|
||||
/* Spacer occupies the same footprint as the SVG folder icon so labels
|
||||
stay vertically aligned across real-folder rows and "(no folder)". */
|
||||
.nav-icon-spacer {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Table view
|
||||
============================================================ */
|
||||
.entry-grid.is-table { display: block; padding: 0; }
|
||||
.entry-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.entry-table thead th {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-faint);
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
user-select: none;
|
||||
position: sticky; top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.entry-table th.is-sortable { cursor: pointer; }
|
||||
.entry-table th.is-sortable:hover { color: var(--text-dim); }
|
||||
.entry-table th.is-active { color: var(--accent); }
|
||||
.entry-table th .sort-arrow {
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.entry-table tbody td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
color: var(--text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.entry-row { cursor: pointer; transition: background var(--t-fast); }
|
||||
.entry-row:hover { background: var(--bg-elev); }
|
||||
.entry-row.is-selected { background: var(--accent-soft); }
|
||||
.entry-row.is-checked { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.col-check { width: 36px; }
|
||||
.col-name { min-width: 180px; }
|
||||
.col-name .entry-avatar-sm {
|
||||
width: 22px; height: 22px;
|
||||
display: inline-grid; place-items: center;
|
||||
font-size: 10px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.col-name .cell-name-wrap { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.col-name .fav-dot { color: var(--warning); font-size: 12px; }
|
||||
.col-site, .col-user { color: var(--text-dim); }
|
||||
.col-site { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.col-user { max-width: 200px; }
|
||||
.col-user .icon-btn { margin-left: 4px; opacity: 0; transition: opacity var(--t-fast); }
|
||||
.entry-row:hover .col-user .icon-btn { opacity: 0.7; }
|
||||
.col-folder { color: var(--text-dim); white-space: nowrap; }
|
||||
.col-updated { color: var(--text-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.col-actions { width: 80px; text-align: right; white-space: nowrap; }
|
||||
.col-actions .icon-btn { opacity: 0; transition: opacity var(--t-fast); }
|
||||
.entry-row:hover .col-actions .icon-btn { opacity: 0.7; }
|
||||
.col-actions .entry-kebab-wrap { display: inline-block; vertical-align: middle; }
|
||||
|
||||
/* Hide Edge/IE native password reveal eye (we ship our own toggle). */
|
||||
input[type="password"]::-ms-reveal,
|
||||
input[type="password"]::-ms-clear {
|
||||
display: none !important;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Login screen pw toggle — only one action button, override the
|
||||
2-buttons layout from .input-with-action. */
|
||||
.input-with-action:has(> .icon-btn:only-of-type) input {
|
||||
padding-right: 40px;
|
||||
}
|
||||
.input-with-action:has(> .icon-btn:only-of-type) .icon-btn:only-of-type {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
/* Custom right-click menu (replaces native Edge contextmenu in inputs). */
|
||||
.custom-ctxmenu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
min-width: 160px;
|
||||
padding: 4px;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.custom-ctxmenu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 7px 12px;
|
||||
font-size: 13px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-ctxmenu-item:hover:not(.is-disabled) { background: var(--bg-elev-2); }
|
||||
.custom-ctxmenu-item.is-disabled { color: var(--text-faint); cursor: default; }
|
||||
|
||||
/* Folder delete X button — visible on row hover only. */
|
||||
.nav-item .folder-delete {
|
||||
margin-left: auto;
|
||||
width: 20px; height: 20px;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: transparent; border: none;
|
||||
color: var(--text-faint);
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity var(--t-fast), color var(--t-fast), background var(--t-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-item .folder-delete svg { width: 14px; height: 14px; display: block; }
|
||||
.nav-item:hover .folder-delete { opacity: 0.7; }
|
||||
.nav-item .folder-delete:hover {
|
||||
opacity: 1;
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
.nav-item:hover .nav-count { display: none; }
|
||||
|
||||
/* Remember username checkbox row on auth screen. */
|
||||
.remember-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin: -4px 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.remember-row input[type="checkbox"] {
|
||||
width: 14px; height: 14px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Just-added entry feedback — pulse background fades out over 2.5s. */
|
||||
@keyframes entryFlash {
|
||||
0% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.3); }
|
||||
40% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.2); }
|
||||
100% { background: var(--bg-elev); box-shadow: 0 0 0 0 transparent; }
|
||||
}
|
||||
.entry-card.is-flash { animation: entryFlash 2.5s ease-out; }
|
||||
.entry-row.is-flash { animation: entryFlash 2.5s ease-out; }
|
||||
|
||||
/* Pagination footer (list + table views). */
|
||||
.pagination {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 14px 12px 4px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.pagination-info {
|
||||
margin-right: auto;
|
||||
color: var(--text-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
.pagination-btn {
|
||||
min-width: 32px;
|
||||
padding: 5px 9px;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all var(--t-fast);
|
||||
}
|
||||
.pagination-btn:hover:not(.is-active):not(.is-disabled) {
|
||||
border-color: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
.pagination-btn.is-active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
cursor: default;
|
||||
}
|
||||
.pagination-btn.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pagination-ellipsis { padding: 0 4px; color: var(--text-faint); }
|
||||
.pagination-size {
|
||||
margin-left: 8px;
|
||||
padding: 5px 8px;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user