feat: v0.3.0 — multiple entries per field, README setup guide
Identity fields now support multiple entries per type: - Add unlimited names (first, last, middle, nickname), emails, usernames, hostnames, and phone numbers per profile - "+ Add" button on each section, "x" to remove rows - Names have a type selector (1st/Last/Mid/Nick) README now includes: - First-time setup walkthrough (step by step) - Icon color legend (gray/black/blue/red) - Keyboard shortcuts table - Note that extension does nothing until configured Also bumps version to 0.3.0 for Firefox re-signing. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
@@ -136,7 +136,7 @@
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>Silent Send v0.2.0</p>
|
||||
<p>Silent Send v0.3.0</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
+49
-1
@@ -310,13 +310,61 @@ body {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.id-label-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.id-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: none;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-add:hover { background: #f3f4f6; color: #111; }
|
||||
|
||||
.id-entry-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.id-entry-row .input-sm { flex: 1; }
|
||||
|
||||
.id-entry-row .btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d1d5db;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.id-entry-row .btn-remove:hover { color: #dc2626; }
|
||||
|
||||
.id-type-label {
|
||||
font-size: 9px;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.id-row {
|
||||
|
||||
+16
-36
@@ -60,55 +60,35 @@
|
||||
<p class="help-text">Each profile is a person/identity. All active profiles are protected simultaneously.</p>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Names</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idFirstReal" placeholder="First name" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idFirstSub" placeholder="Fake first" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idLastReal" placeholder="Last name" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idLastSub" placeholder="Fake last" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-label-row"><span class="id-label">Names</span><button class="btn-add" data-field="names">+ Add</button></div>
|
||||
<div id="idNames"></div>
|
||||
<div class="id-hint">Catches: first last, last first, possessives, case variants</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Email</div>
|
||||
<div class="id-label-row"><span class="id-label">Emails</span><button class="btn-add" data-field="emails">+ Add</button></div>
|
||||
<div id="idEmails"></div>
|
||||
<div class="id-row">
|
||||
<input type="email" id="idEmailReal" placeholder="you@gmail.com" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="email" id="idEmailSub" placeholder="fake@example.com" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for unknown addresses)" class="input" style="flex:1">
|
||||
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for any unrecognized address)" class="input" style="flex:1">
|
||||
</div>
|
||||
<div class="id-hint">Catches: any *@gmail.com, *@yahoo.com, *@outlook.com, etc.</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Username / Computer</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idUserReal" placeholder="jsmith" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idUserSub" placeholder="ademo" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idHostReal" placeholder="macbook-pro" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idHostSub" placeholder="mycomputer" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-hint">Catches: jsmith@macbook-pro, /home/jsmith, ~jsmith, C:\Users\jsmith</div>
|
||||
<div class="id-label-row"><span class="id-label">Usernames</span><button class="btn-add" data-field="usernames">+ Add</button></div>
|
||||
<div id="idUsernames"></div>
|
||||
<div class="id-hint">Catches: user@host, /home/user, ~user, C:\Users\user</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label">Phone</div>
|
||||
<div class="id-row">
|
||||
<input type="text" id="idPhoneReal" placeholder="(555) 123-4567" class="input input-sm">
|
||||
<span class="arrow">→</span>
|
||||
<input type="text" id="idPhoneSub" placeholder="(555) 000-0000" class="input input-sm">
|
||||
</div>
|
||||
<div class="id-label-row"><span class="id-label">Hostnames</span><button class="btn-add" data-field="hostnames">+ Add</button></div>
|
||||
<div id="idHostnames"></div>
|
||||
<div class="id-hint">Catches: hostname in user@hostname and standalone</div>
|
||||
</div>
|
||||
|
||||
<div class="id-section">
|
||||
<div class="id-label-row"><span class="id-label">Phones</span><button class="btn-add" data-field="phones">+ Add</button></div>
|
||||
<div id="idPhones"></div>
|
||||
<div class="id-hint">Catches: all formats — (555) 123-4567, 555-123-4567, 555.123.4567</div>
|
||||
</div>
|
||||
|
||||
|
||||
+148
-65
@@ -191,81 +191,164 @@ function renderProfileSelector() {
|
||||
$('#profileActive').checked = profile?.active ?? true;
|
||||
}
|
||||
|
||||
// --- Identity ---
|
||||
// --- Identity (dynamic multi-entry) ---
|
||||
|
||||
// Field configs for rendering
|
||||
const FIELD_CONFIGS = {
|
||||
names: {
|
||||
container: 'idNames',
|
||||
placeholderReal: 'Real name',
|
||||
placeholderSub: 'Fake name',
|
||||
typeOptions: [{ value: 'first', label: '1st' }, { value: 'last', label: 'Last' }, { value: 'middle', label: 'Mid' }, { value: 'nick', label: 'Nick' }],
|
||||
defaultType: 'first',
|
||||
},
|
||||
emails: {
|
||||
container: 'idEmails',
|
||||
placeholderReal: 'you@gmail.com',
|
||||
placeholderSub: 'fake@example.com',
|
||||
},
|
||||
usernames: {
|
||||
container: 'idUsernames',
|
||||
placeholderReal: 'jsmith',
|
||||
placeholderSub: 'ademo',
|
||||
},
|
||||
hostnames: {
|
||||
container: 'idHostnames',
|
||||
placeholderReal: 'macbook-pro',
|
||||
placeholderSub: 'mycomputer',
|
||||
},
|
||||
phones: {
|
||||
container: 'idPhones',
|
||||
placeholderReal: '(555) 123-4567',
|
||||
placeholderSub: '(555) 000-0000',
|
||||
},
|
||||
};
|
||||
|
||||
function renderFieldList(fieldName, items) {
|
||||
const config = FIELD_CONFIGS[fieldName];
|
||||
const container = $(`#${config.container}`);
|
||||
if (!container) return;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
// Show one empty row
|
||||
items = [{ real: '', substitute: '', type: config.defaultType || '' }];
|
||||
}
|
||||
|
||||
container.innerHTML = items.map((item, i) => {
|
||||
let typeHtml = '';
|
||||
if (config.typeOptions) {
|
||||
typeHtml = `<select class="id-type-select" data-index="${i}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
|
||||
config.typeOptions.map(o =>
|
||||
`<option value="${o.value}" ${item.type === o.value ? 'selected' : ''}>${o.label}</option>`
|
||||
).join('') +
|
||||
`</select>`;
|
||||
}
|
||||
return `<div class="id-entry-row" data-index="${i}">
|
||||
${typeHtml}
|
||||
<input type="text" class="input input-sm id-real" value="${escapeAttr(item.real || '')}" placeholder="${config.placeholderReal}">
|
||||
<span class="arrow" style="font-size:12px">→</span>
|
||||
<input type="text" class="input input-sm id-sub" value="${escapeAttr(item.substitute || '')}" placeholder="${config.placeholderSub}">
|
||||
<button class="btn-remove" title="Remove">×</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Bind remove buttons
|
||||
container.querySelectorAll('.btn-remove').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const row = btn.closest('.id-entry-row');
|
||||
if (container.querySelectorAll('.id-entry-row').length > 1) {
|
||||
row.remove();
|
||||
} else {
|
||||
// Clear instead of remove if it's the last row
|
||||
row.querySelectorAll('input').forEach(inp => inp.value = '');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadIdentityForm() {
|
||||
const profile = profiles.find(p => p.id === currentProfileId);
|
||||
if (!profile) return;
|
||||
|
||||
const first = (profile.names || []).find(n => n.type === 'first');
|
||||
const last = (profile.names || []).find(n => n.type === 'last');
|
||||
const email = (profile.emails || [])[0];
|
||||
const user = (profile.usernames || [])[0];
|
||||
const host = (profile.hostnames || [])[0];
|
||||
const phone = (profile.phones || [])[0];
|
||||
|
||||
$('#idFirstReal').value = first?.real || '';
|
||||
$('#idFirstSub').value = first?.substitute || '';
|
||||
$('#idLastReal').value = last?.real || '';
|
||||
$('#idLastSub').value = last?.substitute || '';
|
||||
$('#idEmailReal').value = email?.real || '';
|
||||
$('#idEmailSub').value = email?.substitute || '';
|
||||
renderFieldList('names', profile.names || []);
|
||||
renderFieldList('emails', profile.emails || []);
|
||||
renderFieldList('usernames', profile.usernames || []);
|
||||
renderFieldList('hostnames', profile.hostnames || []);
|
||||
renderFieldList('phones', profile.phones || []);
|
||||
$('#idCatchAllEmail').value = profile.catchAllEmail || '';
|
||||
$('#idUserReal').value = user?.real || '';
|
||||
$('#idUserSub').value = user?.substitute || '';
|
||||
$('#idHostReal').value = host?.real || '';
|
||||
$('#idHostSub').value = host?.substitute || '';
|
||||
$('#idPhoneReal').value = phone?.real || '';
|
||||
$('#idPhoneSub').value = phone?.substitute || '';
|
||||
$('#profileActive').checked = profile.active ?? true;
|
||||
|
||||
// Bind add buttons
|
||||
$$('.btn-add').forEach(btn => {
|
||||
// Remove old listeners by cloning
|
||||
const newBtn = btn.cloneNode(true);
|
||||
btn.replaceWith(newBtn);
|
||||
newBtn.addEventListener('click', () => {
|
||||
const field = newBtn.dataset.field;
|
||||
const config = FIELD_CONFIGS[field];
|
||||
const container = $(`#${config.container}`);
|
||||
const count = container.querySelectorAll('.id-entry-row').length;
|
||||
const tempDiv = document.createElement('div');
|
||||
let typeHtml = '';
|
||||
if (config.typeOptions) {
|
||||
// Alternate: if first row is 'first', next should be 'last', etc.
|
||||
const defaultType = count % 2 === 0 ? 'first' : 'last';
|
||||
typeHtml = `<select class="id-type-select" data-index="${count}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
|
||||
config.typeOptions.map(o =>
|
||||
`<option value="${o.value}" ${o.value === defaultType ? 'selected' : ''}>${o.label}</option>`
|
||||
).join('') +
|
||||
`</select>`;
|
||||
}
|
||||
tempDiv.innerHTML = `<div class="id-entry-row" data-index="${count}">
|
||||
${typeHtml}
|
||||
<input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}">
|
||||
<span class="arrow" style="font-size:12px">→</span>
|
||||
<input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}">
|
||||
<button class="btn-remove" title="Remove">×</button>
|
||||
</div>`;
|
||||
const row = tempDiv.firstElementChild;
|
||||
container.appendChild(row);
|
||||
row.querySelector('.btn-remove').addEventListener('click', () => {
|
||||
if (container.querySelectorAll('.id-entry-row').length > 1) row.remove();
|
||||
else row.querySelectorAll('input').forEach(inp => inp.value = '');
|
||||
});
|
||||
row.querySelector('.id-real').focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function escapeAttr(str) {
|
||||
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
||||
}
|
||||
|
||||
// Read entries from a dynamic field list
|
||||
function readFieldEntries(fieldName) {
|
||||
const config = FIELD_CONFIGS[fieldName];
|
||||
const container = $(`#${config.container}`);
|
||||
if (!container) return [];
|
||||
|
||||
const entries = [];
|
||||
container.querySelectorAll('.id-entry-row').forEach(row => {
|
||||
const real = row.querySelector('.id-real')?.value.trim() || '';
|
||||
const sub = row.querySelector('.id-sub')?.value.trim() || '';
|
||||
if (real && sub) {
|
||||
const entry = { real, substitute: sub };
|
||||
const typeSelect = row.querySelector('.id-type-select');
|
||||
if (typeSelect) entry.type = typeSelect.value;
|
||||
entries.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function saveIdentity() {
|
||||
const names = [];
|
||||
const firstReal = $('#idFirstReal').value.trim();
|
||||
const firstSub = $('#idFirstSub').value.trim();
|
||||
if (firstReal && firstSub) {
|
||||
names.push({ real: firstReal, substitute: firstSub, type: 'first' });
|
||||
}
|
||||
const lastReal = $('#idLastReal').value.trim();
|
||||
const lastSub = $('#idLastSub').value.trim();
|
||||
if (lastReal && lastSub) {
|
||||
names.push({ real: lastReal, substitute: lastSub, type: 'last' });
|
||||
}
|
||||
|
||||
const emails = [];
|
||||
const emailReal = $('#idEmailReal').value.trim();
|
||||
const emailSub = $('#idEmailSub').value.trim();
|
||||
if (emailReal && emailSub) {
|
||||
emails.push({ real: emailReal, substitute: emailSub });
|
||||
}
|
||||
|
||||
const usernames = [];
|
||||
const userReal = $('#idUserReal').value.trim();
|
||||
const userSub = $('#idUserSub').value.trim();
|
||||
if (userReal && userSub) {
|
||||
usernames.push({ real: userReal, substitute: userSub });
|
||||
}
|
||||
|
||||
const hostnames = [];
|
||||
const hostReal = $('#idHostReal').value.trim();
|
||||
const hostSub = $('#idHostSub').value.trim();
|
||||
if (hostReal && hostSub) {
|
||||
hostnames.push({ real: hostReal, substitute: hostSub });
|
||||
}
|
||||
|
||||
const phones = [];
|
||||
const phoneReal = $('#idPhoneReal').value.trim();
|
||||
const phoneSub = $('#idPhoneSub').value.trim();
|
||||
if (phoneReal && phoneSub) {
|
||||
phones.push({ real: phoneReal, substitute: phoneSub });
|
||||
}
|
||||
|
||||
const profileData = {
|
||||
names,
|
||||
emails,
|
||||
usernames,
|
||||
hostnames,
|
||||
phones,
|
||||
names: readFieldEntries('names'),
|
||||
emails: readFieldEntries('emails'),
|
||||
usernames: readFieldEntries('usernames'),
|
||||
hostnames: readFieldEntries('hostnames'),
|
||||
phones: readFieldEntries('phones'),
|
||||
catchAllEmail: $('#idCatchAllEmail').value.trim(),
|
||||
emailDomains: [],
|
||||
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||
|
||||
Reference in New Issue
Block a user