const API = '/password-manager/api.php';
let token = sessionStorage.getItem('authToken');
let csrfToken = sessionStorage.getItem('csrfToken') || '';
let curUser = sessionStorage.getItem('currentUsername');
function a2b64(arr) { return btoa(String.fromCharCode(...new Uint8Array(arr))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); }
function b642ab(s) { return Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c=>c.charCodeAt(0)).buffer; }
let view = localStorage.getItem('vaultView') || 'grid';
let showView = localStorage.getItem('showViewBtn') !== 'false';
let showMail = localStorage.getItem('showEmail') !== 'false';
let dark = localStorage.getItem('darkTheme') !== 'false';
let lockMin = parseInt(localStorage.getItem('autoLockMinutes') || '5');
let order = JSON.parse(localStorage.getItem('entryOrder') || '[]');
let selectedFolder = localStorage.getItem('selectedFolder') || 'All';
let entries = [];
let folders = ['All'];
let genPwdVal = '';
let cryptoKey = null;
let searchQuery = '';
let idleT, warnT, countT;
let draggedId = null;
let showTrash = false;
let selectedIds = new Set();
let lastSelectedId = null;
let rectState = { active: false, startX: 0, startY: 0, el: null, started: false };
// ==================== SOUND ====================
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
let audioCtx = null;
function getAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
function playTone(freq, duration, type = 'sine', volume = 0.08) {
if (!soundEnabled) return;
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(volume, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + duration);
} catch (e) { /* ignore */ }
}
function playSound(type) {
if (!soundEnabled) return;
switch (type) {
case 'click': playTone(800, 0.08, 'sine', 0.06); break;
case 'success':
playTone(523, 0.1, 'sine', 0.1);
setTimeout(() => playTone(659, 0.1, 'sine', 0.1), 100);
setTimeout(() => playTone(784, 0.15, 'sine', 0.1), 200);
break;
case 'error':
playTone(200, 0.2, 'square', 0.06);
setTimeout(() => playTone(150, 0.3, 'square', 0.06), 150);
break;
case 'delete':
playTone(150, 0.15, 'triangle', 0.08);
break;
case 'copy':
playTone(1200, 0.05, 'sine', 0.07);
break;
case 'generate':
playTone(440, 0.05, 'sine', 0.05);
setTimeout(() => playTone(554, 0.05, 'sine', 0.05), 60);
setTimeout(() => playTone(659, 0.05, 'sine', 0.05), 120);
setTimeout(() => playTone(880, 0.1, 'sine', 0.07), 180);
break;
case 'open':
playTone(600, 0.12, 'sine', 0.06);
setTimeout(() => playTone(800, 0.1, 'sine', 0.06), 80);
break;
case 'close':
playTone(800, 0.08, 'sine', 0.05);
setTimeout(() => playTone(600, 0.1, 'sine', 0.05), 80);
break;
case 'login':
playTone(523, 0.1, 'sine', 0.08);
setTimeout(() => playTone(659, 0.1, 'sine', 0.08), 100);
setTimeout(() => playTone(784, 0.2, 'sine', 0.1), 200);
break;
case 'register':
playTone(440, 0.1, 'sine', 0.08);
setTimeout(() => playTone(554, 0.1, 'sine', 0.08), 100);
setTimeout(() => playTone(659, 0.15, 'sine', 0.1), 200);
break;
}
}
function toggleSound() {
soundEnabled = !soundEnabled;
localStorage.setItem('soundEnabled', soundEnabled);
if (soundEnabled) playTone(440, 0.05);
syncSettingsUI();
}
// ==================== TOAST ====================
function toast(m, t) { t = t || 'success'; const c = document.getElementById('toastContainer'); const d = document.createElement('div'); d.className = 'toast ' + t; d.textContent = m; c.appendChild(d); setTimeout(() => d.remove(), 3000); }
function showZigzagToast(elem, msg, type) {
const t = document.createElement('div');
t.className = 'toast-zigzag ' + (type || 'success');
t.textContent = msg;
document.body.appendChild(t);
const r = elem.getBoundingClientRect();
t.style.left = r.left + 'px';
t.style.top = r.top + 'px';
setTimeout(() => t.remove(), 1500);
}
// ==================== THEME ====================
function applyTheme() { document.body.classList.toggle('light', !dark); }
function toggleTheme() { dark = !dark; localStorage.setItem('darkTheme', dark); applyTheme(); syncSettingsUI(); playSound('click'); }
// ==================== CRYPTO ====================
function checkStrength() { const p = document.getElementById('passwordInput').value; const b = document.getElementById('strengthBar'); let s = 0; if (p.length >= 8) s++; if (p.length >= 12) s++; if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++; if (/\d/.test(p)) s++; if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++; b.className = 'strength-bar s' + Math.min(4, s); }
async function deriveKey(pwd, salt) { const enc = new TextEncoder(); const km = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveKey']); const sb = Uint8Array.from(atob(salt), c => c.charCodeAt(0)); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: sb, iterations: 100000, hash: 'SHA-256' }, km, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); }
async function encryptPwd(plain) { const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plain)); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(enc))), iv: btoa(String.fromCharCode(...iv)) }; }
async function decryptPwd(encB64, ivB64) { try { const enc = Uint8Array.from(atob(encB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, enc); return new TextDecoder().decode(dec); } catch (e) { return '[ERROR]'; } }
async function persistCryptoKey() { const raw = await crypto.subtle.exportKey('raw', cryptoKey); sessionStorage.setItem('cryptoKey', btoa(String.fromCharCode(...new Uint8Array(raw)))); }
async function restoreCryptoKey() { const saved = sessionStorage.getItem('cryptoKey'); if (!saved) return false; try { const raw = Uint8Array.from(atob(saved), c => c.charCodeAt(0)); cryptoKey = await crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); return true; } catch (e) { return false; } }
// ==================== AUTO-LOCK ====================
function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTimer').value); localStorage.setItem('autoLockMinutes', lockMin); resetIdle(); }
function resetIdle() { clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT); document.getElementById('idleWarning').classList.remove('show'); if (lockMin > 0 && token) { const lm = lockMin * 60000; warnT = setTimeout(() => { document.getElementById('idleWarning').classList.add('show'); let cd = 30; document.getElementById('idleCountdown').textContent = cd; countT = setInterval(() => { cd--; document.getElementById('idleCountdown').textContent = cd; if (cd <= 0) { clearInterval(countT); doLogout(); } }, 1000); }, Math.max(0, lm - 30000)); idleT = setTimeout(() => doLogout(), lm); } }
// ==================== USERNAME ====================
function saveUsername() { const f = document.getElementById('addUsername'); if (f && f.value.trim()) localStorage.setItem('savedUsername', f.value.trim()); }
function loadUsername() { const s = localStorage.getItem('savedUsername'); const f = document.getElementById('addUsername'); if (s && f) f.value = s; }
// ==================== FOLDERS ====================
async function loadFolders() {
if (!token) return;
try {
const r = await fetch(API + '/folders', { headers: { 'Authorization': 'Bearer ' + token } });
if (r.ok) {
const data = await r.json();
if (Array.isArray(data)) {
folders = data.filter(f => typeof f === 'string');
if (!folders.includes('All')) folders.unshift('All');
} else {
folders = ['All'];
}
} else {
folders = ['All'];
}
} catch (e) {
folders = ['All'];
}
}
async function addFolderToServer(name) {
try {
const r = await fetch(API + '/folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ name })
});
if (r.ok) { await loadFolders(); return true; }
const d = await r.json();
toast('❌ ' + (d.error || 'Error'), 'error');
return false;
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
}
async function deleteFolderFromServer(name) {
try {
const r = await fetch(API + '/folders/' + encodeURIComponent(name), {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }
});
if (r.ok) {
await loadFolders();
if (selectedFolder === name) { selectedFolder = 'All'; localStorage.setItem('selectedFolder', 'All'); }
return true;
}
const d = await r.json();
toast('❌ ' + (d.error || 'Error'), 'error');
return false;
} catch (e) { toast('⚠️ Connection error', 'error'); return false; }
}
function folderColor(name) {
if (name === 'All') return '';
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
const hue = ((hash % 360) + 360) % 360;
return `style="--chip-color:hsl(${hue},60%,55%)"`;
}
function renderFolders() {
const bar = document.getElementById('foldersBar');
if (!bar) return;
entries.forEach(e => { if (!e.folder || typeof e.folder !== 'string') e.folder = 'All'; });
const counts = {};
entries.forEach(e => { const f = e.folder; counts[f] = (counts[f] || 0) + 1; });
let html = '';
folders.forEach(f => {
if (!f) return;
const count = counts[f] || 0;
const color = folderColor(f);
html += `📁 ${esc(f)}${count}${f !== 'All' ? `` : ''}`;
});
html += ``;
bar.innerHTML = html;
// Make folders drop targets for moving entries
bar.querySelectorAll('.folder-chip[data-folder]').forEach(chip => {
chip.addEventListener('dragover', e => { e.preventDefault(); chip.classList.add('drag-over'); });
chip.addEventListener('dragleave', () => chip.classList.remove('drag-over'));
chip.addEventListener('drop', async function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const id = parseInt(e.dataTransfer.getData('text/plain'));
if (!id) return;
const entry = entries.find(x => x.id === id);
if (!entry) return;
const folder = this.dataset.folder;
const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id];
let moved = 0;
for (const sid of ids) {
const e2 = entries.find(x => x.id === sid);
if (!e2 || e2.folder === folder) continue;
const enc = await encryptPwd(e2.password);
const r = await fetch(API + '/entries/' + sid, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ site: e2.site, username: e2.username, encrypted_password: enc.encrypted, iv: enc.iv, folder })
});
if (r.ok) { e2.folder = folder; moved++; }
}
renderFolders();
render();
if (moved) playSound('success');
});
});
}
function selectFolder(f) {
selectedFolder = f;
localStorage.setItem('selectedFolder', f);
renderFolders();
populateFolderSelects();
render();
playSound('click');
}
function showAddFolderModal() {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
document.getElementById('cancelAddFolder').onclick = () => overlay.remove();
document.getElementById('confirmAddFolder').onclick = async () => {
const name = document.getElementById('newFolderName').value.trim();
if (!name) { toast('Enter a name', 'error'); return; }
const ok = await addFolderToServer(name);
if (ok) { renderFolders(); populateFolderSelects(); overlay.remove(); toast('📁 Folder created!'); playSound('success'); }
};
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
playSound('open');
}
function showDeleteFolderConfirm(folderName) {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = `🗑️ Delete Folder
Delete "${folderName}"? Entries move to "All".
`;
document.body.appendChild(overlay);
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
document.getElementById('confirmDeleteFolder').onclick = async () => {
const ok = await deleteFolderFromServer(folderName);
if (ok) { renderFolders(); populateFolderSelects(); render(); overlay.remove(); toast('📁 Folder deleted'); playSound('delete'); }
};
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
}
// ==================== TRASH ====================
function toggleTrash() {
showTrash = !showTrash;
const btn = document.getElementById('trashBtn');
const actions = document.getElementById('trashActions');
if (btn) { btn.textContent = showTrash ? '📋 Active' : '🗑️ Trash'; btn.classList.toggle('btn-danger', showTrash); }
if (actions) actions.classList.toggle('hidden', !showTrash);
loadEntries();
playSound('click');
}
async function restoreEntry(id) {
try { const r = await fetch(API + '/entries/' + id + '/restore', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { toast('✅ Restored!'); await loadEntries(); playSound('success'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
async function toggleFavorite(id) {
try { await fetch(API + '/entries/' + id + '/favorite', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); const e = entries.find(x => x.id == id); if (e) e.favorite = e.favorite ? 0 : 1; renderFolders(); render(); playSound('click'); } catch (e) {}
}
async function permanentDelete(id) {
if (!confirm('Permanently delete?')) return;
try { const r = await fetch(API + '/entries/' + id + '?permanent=1', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { toast('🗑️ Permanently deleted'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
async function emptyTrash() {
if (!confirm('Delete ALL trashed entries?')) return;
try { const r = await fetch(API + '/entries/trash/empty', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); if (r.ok) { toast('🗑️ Trash emptied'); await loadEntries(); playSound('error'); } } catch (e) { toast('⚠️ Error', 'error'); }
}
function timeAgo(dateStr) {
if (!dateStr) return '';
const now = new Date(); const d = new Date(dateStr + 'Z');
const days = 30 - Math.floor((now - d) / (1000 * 60 * 60 * 24));
return days <= 0 ? 'Expiring' : days + 'd left';
}
// ==================== SETTINGS ====================
function toggleSettings() {
document.getElementById('settingsMenu').classList.toggle('hidden');
}
function syncSettingsUI() {
document.getElementById('soundToggleSwitch').classList.toggle('active', soundEnabled);
document.getElementById('themeToggleSwitch').classList.toggle('active', !dark);
document.getElementById('showViewBtnToggle').classList.toggle('active', showView);
document.getElementById('showEmailToggle').classList.toggle('active', showMail);
document.getElementById('autoLockTimer').value = lockMin;
}
async function registerPasskey() {
try {
const r = await fetch(API + '/passkey/register/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }
});
if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); return; }
const opts = await r.json();
opts.challenge = b642ab(opts.challenge);
opts.user.id = b642ab(opts.user.id);
if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; }
const cred = await navigator.credentials.create({ publicKey: opts });
const result = {
id: cred.id,
response: {
clientDataJSON: a2b64(cred.response.clientDataJSON),
attestationObject: a2b64(cred.response.attestationObject)
}
};
const r2 = await fetch(API + '/passkey/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify(result)
});
if (r2.ok) { toast('✅ Passkey registered!'); playSound('success'); }
else { const d = await r2.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Passkey setup failed: ' + e.message, 'error'); }
}
// ==================== INIT ====================
function init() {
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
document.getElementById('autoLockTimer').value = lockMin;
applyTheme();
// document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
loadUsername();
const sl = localStorage.getItem('savedLoginUser');
if (sl) document.getElementById('loginUsername').value = sl;
syncSettingsUI();
document.getElementById('viewToggle').addEventListener('click', e => {
if (e.target.classList.contains('view-btn')) {
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
view = e.target.dataset.view;
localStorage.setItem('vaultView', view);
render();
playSound('click');
}
});
const clearBtn = document.getElementById('clearSearchBtn');
if (clearBtn) clearBtn.style.display = 'none';
document.getElementById('addModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeAdd();
});
// Close settings when clicking outside
document.addEventListener('click', (e) => {
const menu = document.getElementById('settingsMenu');
const btn = document.getElementById('settingsBtn');
if (menu && !menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) {
menu.classList.add('hidden');
}
});
}
function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); syncSettingsUI(); render(); playSound('click'); }
function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); syncSettingsUI(); render(); playSound('click'); }
// ==================== GENERATOR ====================
function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); }
function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); }
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
function genPreset(len, chars) {
document.getElementById('pwdLen').value = len;
document.getElementById('lenVal').textContent = len;
document.getElementById('useUpper').checked = chars.includes('upper');
document.getElementById('useLower').checked = chars.includes('lower');
document.getElementById('useNum').checked = chars.includes('num');
document.getElementById('useSym').checked = chars.includes('sym');
genPwd();
playSound('click');
}
function genPwd() { const l = parseInt(document.getElementById('pwdLen').value); let c = ''; if (document.getElementById('useUpper').checked) c += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (document.getElementById('useLower').checked) c += 'abcdefghijklmnopqrstuvwxyz'; if (document.getElementById('useNum').checked) c += '0123456789'; if (document.getElementById('useSym').checked) c += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!c) { document.getElementById('genPreview').textContent = 'Select option'; return; } let p = ''; const arr = new Uint32Array(l); crypto.getRandomValues(arr); for (let i = 0; i < l; i++) p += c.charAt(arr[i] % c.length); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
function useGen() {
if (!genPwdVal) genPwd();
// Put the generated password into the add‑modal’s password field
const pwdField = document.getElementById('addPassword');
if (pwdField) {
pwdField.value = genPwdVal;
checkAddStrength(); // update the strength bar
}
navigator.clipboard.writeText(genPwdVal);
toast('🎲 Copied!');
closeGen(); // closes the generator modal, not the add modal
}
function populateFolderSelects() {
['addFolder', 'editFolder'].forEach(id => {
const select = document.getElementById(id);
if (!select) return;
select.innerHTML = '';
folders.forEach(f => {
if (!f) return;
const option = document.createElement('option');
option.value = f;
option.textContent = '📁 ' + f;
if (f === selectedFolder) option.selected = true;
select.appendChild(option);
});
});
}
function openAdd() {
document.getElementById('addSite').value = '';
document.getElementById('addPassword').value = '';
document.getElementById('addStrengthBar').className = 'strength-bar s0';
loadUsername();
populateFolderSelects();
document.getElementById('addModal').classList.add('show');
playSound('open');
}
function closeAdd() {
document.getElementById('addModal').classList.remove('show');
playSound('close');
}
function checkRegStrength() {
const p = document.getElementById('regPassword').value;
const bar = document.getElementById('regStrengthBar');
let s = 0;
if (p.length >= 8) s++;
if (p.length >= 12) s++;
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++;
if (/\d/.test(p)) s++;
if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++;
bar.className = 'strength-bar s' + Math.min(4, s);
}
function checkAddStrength() {
const p = document.getElementById('addPassword').value;
const bar = document.getElementById('addStrengthBar');
let s = 0;
if (p.length >= 8) s++;
if (p.length >= 12) s++;
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++;
if (/\d/.test(p)) s++;
if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(p)) s++;
bar.className = 'strength-bar s' + Math.min(4, s);
}
// ==================== AUTH ====================
function switchTab(t) { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); event.target.classList.add('active'); document.getElementById('loginForm').classList.toggle('hidden', t !== 'login'); document.getElementById('registerForm').classList.toggle('hidden', t !== 'register'); }
async function loginWithPasskey() {
if (!window.PublicKeyCredential) { toast('❌ Passkeys not supported', 'error'); return; }
const u = document.getElementById('loginUsername').value.trim();
if (!u) { toast('Enter username first', 'error'); return; }
document.getElementById('loginBtn').disabled = true;
try {
const r = await fetch(API + '/passkey/login/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: u })
});
if (!r.ok) { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); document.getElementById('loginBtn').disabled = false; return; }
const opts = await r.json();
opts.challenge = b642ab(opts.challenge);
opts.allowCredentials.forEach(c => { c.id = b642ab(c.id); });
const cred = await navigator.credentials.get({ publicKey: opts });
const result = {
id: cred.id,
response: {
clientDataJSON: a2b64(cred.response.clientDataJSON),
authenticatorData: a2b64(cred.response.authenticatorData),
signature: a2b64(cred.response.signature),
userHandle: cred.response.userHandle ? a2b64(cred.response.userHandle) : null
}
};
const r2 = await fetch(API + '/passkey/login/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
});
const d = await r2.json();
if (r2.ok) {
token = d.token; csrfToken = d.csrfToken || ''; curUser = d.username || u;
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('csrfToken', csrfToken);
sessionStorage.setItem('currentUsername', curUser);
// Try to restore crypto key from sessionStorage
const restored = await restoreCryptoKey();
if (!restored) {
// Need master password once to derive crypto key
const mp = await new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = ``;
document.body.appendChild(overlay);
document.getElementById('passkeyTempBtn').onclick = () => resolve(document.getElementById('passkeyTempPwd').value);
overlay.addEventListener('keydown', function handler(e) { if (e.key === 'Enter') { resolve(document.getElementById('passkeyTempPwd').value); overlay.remove(); document.removeEventListener('keydown', handler); } });
});
const m = document.querySelector('.custom-modal-overlay.show');
if (m) m.remove();
cryptoKey = await deriveKey(mp, d.salt);
persistCryptoKey();
}
await loadFolders();
toast('✅ Biometric login!');
playSound('login');
showVault();
loadEntries();
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Passkey login failed: ' + e.message, 'error'); }
finally { document.getElementById('loginBtn').disabled = false; }
}
async function login() {
const u = document.getElementById('loginUsername').value.trim();
const p = document.getElementById('loginPassword').value;
if (!u || !p) { toast('Fill all fields', 'error'); return; }
document.getElementById('loginBtn').disabled = true;
localStorage.setItem('savedLoginUser', u);
try {
const r = await fetch(API + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
const d = await r.json();
if (r.ok) {
token = d.token; csrfToken = d.csrfToken || ''; curUser = u;
cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('csrfToken', csrfToken);
sessionStorage.setItem('currentUsername', u);
await loadFolders();
toast('✅ Login!');
playSound('login');
showVault();
loadEntries();
} else { toast('❌ ' + (d.error || 'Invalid'), 'error'); document.getElementById('loginPassword').value = ''; }
} catch (e) { toast('⚠️ Connection error', 'error'); }
finally { document.getElementById('loginBtn').disabled = false; }
}
async function register() {
const u = document.getElementById('regUsername').value.trim();
const p = document.getElementById('regPassword').value;
if (u.length < 3) { toast('Username min 3', 'error'); return; }
if (p.length < 8) { toast('Password min 8', 'error'); return; }
document.getElementById('registerBtn').disabled = true;
try {
const r = await fetch(API + '/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: u, masterPassword: p }) });
const d = await r.json();
if (r.ok) {
token = d.token; csrfToken = d.csrfToken || ''; curUser = u;
cryptoKey = await deriveKey(p, d.salt);
persistCryptoKey();
sessionStorage.setItem('authToken', token);
sessionStorage.setItem('csrfToken', csrfToken);
sessionStorage.setItem('currentUsername', u);
await loadFolders();
toast('✅ Created!');
playSound('register');
showVault();
loadEntries();
} else { toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Connection error', 'error'); }
finally { document.getElementById('registerBtn').disabled = false; }
}
async function doLogout() {
saveUsername();
if (token) {
try { await fetch(API + '/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } }); } catch (e) {}
}
clearTimeout(idleT); clearTimeout(warnT); clearInterval(countT);
document.getElementById('idleWarning').classList.remove('show');
token = null; csrfToken = ''; curUser = null; entries = []; cryptoKey = null; folders = ['All']; showTrash = false;
sessionStorage.clear();
document.getElementById('authSection').classList.remove('hidden');
document.getElementById('vaultSection').classList.add('hidden');
document.getElementById('loginPassword').value = '';
document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
}
function showVault() {
document.getElementById('authSection').classList.add('hidden');
document.getElementById('vaultSection').classList.remove('hidden');
document.getElementById('currentUser').textContent = '👤 ' + curUser;
//document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
document.getElementById('autoLockTimer').value = lockMin;
loadUsername();
renderFolders();
populateFolderSelects();
syncSettingsUI();
resetIdle();
}
// ==================== ENTRIES ====================
function applyOrder(list) { if (!list || !list.length) return []; if (!order || !order.length) return list; const map = new Map(list.filter(e => e && e.id).map(e => [e.id, e])); const ord = []; order.forEach(id => { if (map.has(id)) { ord.push(map.get(id)); map.delete(id); } }); map.forEach(e => ord.push(e)); return ord; }
async function loadEntries(q) {
try {
let url = API + '/entries?deleted=' + (showTrash ? '1' : '0');
if (q) url += '&search=' + encodeURIComponent(q);
const r = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token } });
if (r.ok) {
const raw = await r.json();
entries = [];
for (const e of raw) {
if (e.encryption_method === 'client') {
const pw = await decryptPwd(e.encrypted_password, e.iv);
entries.push({ id: e.id, site: e.site, username: e.username, password: pw, folder: e.folder || 'All', deleted_at: e.deleted_at, favorite: e.favorite || 0 });
} else {
entries.push({ id: e.id, site: e.site, username: e.username, password: e.password || '', folder: e.folder || 'All', deleted_at: e.deleted_at, favorite: e.favorite || 0 });
}
}
entries = applyOrder(entries);
entries.sort((a, b) => (b.favorite || 0) - (a.favorite || 0));
document.getElementById('connectionStatus').textContent = '🟢 Connected';
document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)';
renderFolders();
populateFolderSelects();
render();
} else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); }
} catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); }
}
function getFilteredEntries() {
if (showTrash) return entries;
if (selectedFolder === 'All') return entries;
return entries.filter(e => (e.folder || 'All') === selectedFolder);
}
// ==================== RENDER ====================
function render() {
const c = document.getElementById('entriesContainer');
c.className = '';
if (showTrash) c.classList.add('trash-view');
c.classList.add(view + '-view');
const filtered = getFilteredEntries();
if (!filtered.length) { c.innerHTML = '' + (showTrash ? '📭 Trash empty' : '📭 No entries') + '
'; return; }
if (view === 'table') {
let h = ' | Site | ' + (showMail ? 'User | ' : '') + 'Password | ' + (!showTrash ? 'Folder | ' : 'Deleted | ') + 'Actions |
';
filtered.forEach(e => {
const sel = selectedIds.has(e.id);
h += '' +
'| ' + (e.favorite ? '⭐' : '') + ' | ' +
'🌐 ' + highlightText(e.site, searchQuery) + ' | ' +
(showMail ? '👤 ' + highlightText(e.username, searchQuery) + ' | ' : '') +
'•••••••• | ';
if (!showTrash) {
h += '📁 ' + esc(e.folder || 'All') + ' | ' +
'' +
' ' +
' ' +
' ' +
'' +
' | ';
} else {
h += '🗑️ ' + timeAgo(e.deleted_at) + ' | ' +
'' +
' ' +
'' +
' | ';
}
h += '
';
});
h += '
';
c.innerHTML = h;
} else {
c.innerHTML = filtered.map(e => {
if (view === 'grid') return gridC(e);
if (view === 'compact') return compC(e);
return listC(e);
}).join('');
}
attachEvents();
setupDrag();
}
function gridC(e) {
const sel = selectedIds.has(e.id);
let html = '';
html += '
';
if (showTrash) {
html += '';
html += '';
} else {
html += '';
html += '';
html += '';
}
html += '
';
html += '
🌐 ' + highlightText(e.site, searchQuery) + '
';
if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '
';
html += '
📁 ' + esc(e.folder || 'All') + '
';
if (!showTrash) {
html += '
';
} else {
html += '
🗑️ ' + timeAgo(e.deleted_at) + '
';
}
html += '
';
return html;
}
function listC(e) {
const sel = selectedIds.has(e.id);
let html = '';
html += '
';
if (showTrash) {
html += '';
html += '';
} else {
html += '';
html += '';
html += '';
}
html += '
';
html += '
🌐 ' + highlightText(e.site, searchQuery) + '';
if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '
📁 ' + esc(e.folder || 'All') + '';
html += '
••••••••' +
'
';
} else {
html += '
🗑️ ' + timeAgo(e.deleted_at) + '';
}
html += '
';
return html;
}
function compC(e) {
const sel = selectedIds.has(e.id);
let html = '';
html += '
';
if (showTrash) {
html += '';
html += '';
} else {
html += '';
html += '';
html += '';
}
html += '
';
html += '
🌐 ' + highlightText(e.site, searchQuery) + '';
if (showMail) html += '
👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '
📁 ' + esc(e.folder || 'All') + '';
html += '
••••••••';
html += '
';
} else {
html += '
🗑️ ' + timeAgo(e.deleted_at) + '';
}
html += '
';
return html;
}
// ==================== EVENTS ====================
function showConfirm(btn, message, callback) {
const id = btn.dataset.id;
const existing = document.querySelector('.custom-confirm');
if (existing) existing.remove();
const confirm = document.createElement('div');
confirm.className = 'custom-confirm show';
confirm.innerHTML =
'' + message + '
' +
'' +
'' +
'' +
'
';
document.body.appendChild(confirm);
const rect = btn.getBoundingClientRect();
confirm.style.top = (rect.top - 60) + 'px';
let leftPos = rect.left - confirm.offsetWidth + rect.width;
if (leftPos < 10) leftPos = 10;
confirm.style.left = leftPos + 'px';
const yesBtn = confirm.querySelector('.confirm-yes');
const noBtn = confirm.querySelector('.confirm-no');
const cleanup = () => {
confirm.remove();
document.removeEventListener('keydown', keyHandler);
};
const keyHandler = (e) => {
if (e.key === 'Enter' || e.key === 'y' || e.key === 'Y') {
e.preventDefault();
cleanup();
callback(id);
showZigzagToast(btn, '🗑️ Deleted!', 'error');
playSound('delete');
} else if (e.key === 'Escape' || e.key === 'n' || e.key === 'N') {
e.preventDefault();
cleanup();
}
};
yesBtn.onclick = () => {
cleanup();
callback(id);
showZigzagToast(btn, '🗑️ Deleted!', 'error');
playSound('delete');
};
noBtn.onclick = () => cleanup();
// Focus the confirm box so keyboard events are captured
confirm.tabIndex = 0;
confirm.focus();
document.addEventListener('keydown', keyHandler);
// Close if clicking outside
setTimeout(() => {
document.addEventListener('click', function closeConfirm(e) {
if (!confirm.contains(e.target) && e.target !== btn) {
cleanup();
document.removeEventListener('click', closeConfirm);
}
});
}, 10);
}
function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.password : ''; }
function attachEvents() {
document.querySelectorAll('.delete-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); if (showTrash) { permanentDelete(this.dataset.id); } else { showConfirm(this, 'Delete this entry?', id => delEntry(id)); } });
document.querySelectorAll('.edit-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); openEdit(this.dataset.id); });
document.querySelectorAll('.star-btn').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); toggleFavorite(this.dataset.id); });
document.querySelectorAll('.copy-p').forEach(b => b.onclick = async function(ev) { ev.stopPropagation(); const pw = entryPw(this.dataset.id); try { await navigator.clipboard.writeText(pw); this.textContent = '✓'; const btn = this; setTimeout(() => { btn.textContent = '📋'; }, 1000); showZigzagToast(this, '📋 Copied!', 'success'); playSound('copy'); } catch (e) { showZigzagToast(this, 'Failed', 'error'); } });
// Password reveal on hover
if (showView) {
document.querySelectorAll('.pw-display.pw-hover').forEach(el => {
el.addEventListener('mouseenter', function() {
const pw = entryPw(this.id.replace('p-', ''));
this.textContent = pw;
});
el.addEventListener('mouseleave', function() {
this.textContent = '••••••••';
});
});
}
}
function setupDrag() {
const c = document.getElementById('entriesContainer'); if (!c) return;
c.querySelectorAll('[draggable="true"]').forEach(el => {
el.ondragstart = function(e) { draggedId = this.dataset.id; const dragIds = selectedIds.has(parseInt(draggedId)) && selectedIds.size > 1 ? [...selectedIds] : [parseInt(draggedId)]; c.querySelectorAll('[draggable="true"]').forEach(card => { card.classList.toggle('drag-dim', dragIds.includes(parseInt(card.dataset.id))); }); e.dataTransfer.setData('text/plain', this.dataset.id); e.dataTransfer.effectAllowed = 'move'; if (dragIds.length > 1) { const cv = document.createElement('canvas'); cv.width = 100; cv.height = 50; const g = cv.getContext('2d'); for (let i = dragIds.length - 1; i >= 0; i--) { const ox = i * 4, oy = i * 4; g.fillStyle = i === 0 ? 'rgba(30,40,55,0.9)' : 'rgba(59,130,246,0.15)'; g.fillRect(ox, oy, 80, 36); g.strokeStyle = 'rgba(255,255,255,0.15)'; g.strokeRect(ox, oy, 80, 36); } g.fillStyle = 'rgba(0,0,0,0.7)'; g.fillRect(0, 34, 100, 16); g.fillStyle = '#fff'; g.font = '11px sans-serif'; g.textAlign = 'center'; g.fillText(dragIds.length + ' items', 50, 46); cv.style.position = 'fixed'; cv.style.top = '-1000px'; document.body.appendChild(cv); e.dataTransfer.setDragImage(cv, 6, 10); setTimeout(() => cv.remove(), 50); } };
el.ondragend = function(e) { c.querySelectorAll('.drag-dim').forEach(card => card.classList.remove('drag-dim')); draggedId = null; c.querySelectorAll('.drag-over').forEach(x => x.classList.remove('drag-over')); document.getElementById('trashBtn')?.classList.remove('drag-over'); };
el.ondragover = function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dataset.id !== draggedId) this.classList.add('drag-over'); };
el.ondragleave = function(e) { this.classList.remove('drag-over'); };
el.ondrop = function(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove('drag-over'); const fromId = parseInt(e.dataTransfer.getData('text/plain')); const toId = parseInt(this.dataset.id); if (!fromId || !toId) return; const ids = selectedIds.has(fromId) && selectedIds.size > 1 ? [...selectedIds] : [fromId]; if (ids.length === 1 && ids[0] === toId) return; const base = order.length > 0 ? order : entries.filter(e => e).map(e => e.id); const filtered = base.filter(id => !ids.includes(id)); const idx = filtered.indexOf(toId); idx > -1 ? filtered.splice(idx, 0, ...ids) : filtered.push(...ids); order = filtered; localStorage.setItem('entryOrder', JSON.stringify(order)); const map = new Map(entries.filter(e => e).map(e => [e.id, e])); entries = order.map(id => map.get(id)).filter(e => e); entries.sort((a, b) => (b.favorite || 0) - (a.favorite || 0)); render(); };
});
}
// ==================== EDIT ====================
function openEdit(id) {
let e = null;
for (let i = 0; i < entries.length; i++) { if (entries[i] && entries[i].id == id) { e = entries[i]; break; } }
if (!e) return;
const folderSelect = document.getElementById('editFolder');
folderSelect.innerHTML = '';
folders.forEach(f => {
if (!f) return;
const option = document.createElement('option');
option.value = f;
option.textContent = '📁 ' + f;
if (f === (e.folder || 'All')) option.selected = true;
folderSelect.appendChild(option);
});
document.getElementById('editId').value = id;
document.getElementById('editSite').value = e.site;
document.getElementById('editUsername').value = e.username;
document.getElementById('editPassword').value = e.password;
document.getElementById('editPassword').type = 'password';
document.getElementById('editModal').classList.add('show');
playSound('open');
}
function closeEdit() { document.getElementById('editModal').classList.remove('show'); playSound('close'); }
function toggleEditPassword() { const f = document.getElementById('editPassword'); f.type = f.type === 'password' ? 'text' : 'password'; }
async function saveEdit() {
const id = document.getElementById('editId').value;
const site = document.getElementById('editSite').value.trim();
const username = document.getElementById('editUsername').value.trim();
const password = document.getElementById('editPassword').value;
const folder = document.getElementById('editFolder').value;
if (!site || !password) { toast('Site and password required', 'error'); return; }
try {
const enc = await encryptPwd(password);
const r = await fetch(API + '/entries/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ site, username, encrypted_password: enc.encrypted, iv: enc.iv, folder }) });
if (r.ok) { toast('✅ Updated!'); closeEdit(); loadEntries(); playSound('success'); }
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
} catch (e) { toast('⚠️ Error', 'error'); }
}
//======================== batch selection ==========================
function toggleSelectEntry(id, e) {
if (e?.shiftKey && lastSelectedId !== null) {
const ids = getFilteredEntries().map(x => x.id);
const i1 = ids.indexOf(lastSelectedId);
const i2 = ids.indexOf(id);
if (i1 > -1 && i2 > -1) {
const start = Math.min(i1, i2), end = Math.max(i1, i2);
for (let i = start; i <= end; i++) selectedIds.add(ids[i]);
}
} else if (e?.ctrlKey || e?.metaKey) {
if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id);
} else {
if (selectedIds.size === 1 && selectedIds.has(id)) { selectedIds.clear(); }
else { selectedIds.clear(); selectedIds.add(id); }
}
lastSelectedId = id;
updateBatchBar();
render();
}
function clearSelection() {
selectedIds.clear();
lastSelectedId = null;
hideBatchBar();
render();
}
function updateBatchBar() {
const existing = document.getElementById('batchBar');
if (existing) existing.remove();
if (selectedIds.size === 0) return;
const bar = document.createElement('div');
bar.id = 'batchBar';
bar.className = 'batch-actions';
bar.innerHTML = `${selectedIds.size} selected`;
if (showTrash) {
bar.innerHTML += `
`;
} else {
bar.innerHTML += `
`;
}
bar.innerHTML += ``;
document.body.appendChild(bar);
}
function hideBatchBar() {
const bar = document.getElementById('batchBar');
if (bar) bar.remove();
}
async function batchDelete() {
if (!confirm(`Move ${selectedIds.size} entries to trash?`)) return;
for (const id of selectedIds) await delEntry(id);
clearSelection();
}
async function batchPermanentDelete() {
if (!confirm(`Permanently delete ${selectedIds.size} entries?`)) return;
for (const id of selectedIds) await permanentDelete(id);
clearSelection();
}
async function batchRestore() {
for (const id of selectedIds) await restoreEntry(id);
clearSelection();
}
async function batchMove() {
const folder = document.getElementById('batchFolder')?.value || 'All';
for (const id of selectedIds) {
const e = entries.find(x => x.id == id);
if (e) {
e.folder = folder;
const enc = await encryptPwd(e.password);
await fetch(API + '/entries/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ site: e.site, username: e.username, encrypted_password: enc.encrypted, iv: enc.iv, folder })
});
}
}
clearSelection();
loadEntries();
}
// ==================== ADD / DELETE ====================
async function addEntry() {
const site = document.getElementById('addSite').value.trim();
const user = document.getElementById('addUsername').value.trim();
const pass = document.getElementById('addPassword').value;
if (!site || !pass) { toast('❌ Site and password required', 'error'); return; }
if (user) localStorage.setItem('savedUsername', user);
const btn = document.getElementById('addEntryBtn');
btn.disabled = true;
try {
const enc = await encryptPwd(pass);
const folder = document.getElementById('addFolder').value;
const r = await fetch(API + '/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, encryption_method: 'client', folder })
});
if (r.ok) {
closeAdd();
toast('✅ Saved!');
loadEntries();
playSound('success');
} else {
const d = await r.json();
toast('❌ ' + (d.error || 'Failed'), 'error');
}
} catch (e) { toast('⚠️ Error', 'error'); }
finally { btn.disabled = false; }
}
async function delEntry(id) {
try {
const r = await fetch(API + '/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken } });
if (r.ok) { selectedIds.delete(id); order = order.filter(x => x != id); localStorage.setItem('entryOrder', JSON.stringify(order)); toast('📦 Moved to trash'); await loadEntries(); playSound('delete'); }
} catch (e) { toast('Error', 'error'); }
}
// ==================== UTILS ====================
function searchEntries() {
const input = document.getElementById('searchInput');
searchQuery = input.value.trim();
const btn = document.getElementById('clearSearchBtn');
if (btn) btn.style.display = searchQuery ? 'block' : 'none';
loadEntries(searchQuery);
}
function showExportModal() {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = ``;
document.body.appendChild(overlay);
document.getElementById('cancelExport').onclick = () => overlay.remove();
document.getElementById('confirmExport').onclick = async () => {
const pwd = document.getElementById('exportPassword').value;
if (!pwd) { toast('Enter your master password', 'error'); return; }
try {
const r = await fetch(API + '/reauth', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ masterPassword: pwd })
});
if (r.ok) {
overlay.remove();
const b = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = 'vault-' + new Date().toISOString().slice(0, 10) + '.json';
a.click();
URL.revokeObjectURL(a.href);
toast('Exported!');
playSound('success');
} else {
toast('❌ Invalid password', 'error');
}
} catch (e) { toast('⚠️ Error', 'error'); }
};
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
playSound('open');
}
function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
function escRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function highlightText(text, query) {
if (!query || !query.trim()) return esc(text);
const re = new RegExp('(' + escRegex(query.trim()) + ')', 'gi');
return esc(text).replace(re, '$1');
}
function showShortcutsHelp() {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay show';
overlay.innerHTML = `⌨️ Keyboard Shortcuts
Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / deselect?Show this help
💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function clearSearch() {
const input = document.getElementById('searchInput');
input.value = '';
searchQuery = '';
const btn = document.getElementById('clearSearchBtn');
if (btn) btn.style.display = 'none';
loadEntries();
input.focus();
}
// ==================== STARTUP – session persistence ====================
init();
applyTheme();
if (token && curUser) {
(async () => {
if (await restoreCryptoKey()) {
await loadFolders();
showVault();
loadEntries();
} else {
sessionStorage.clear();
token = null;
curUser = null;
document.getElementById('loginUsername').value = localStorage.getItem('savedLoginUser') || '';
}
})();
}
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
// Trash button as drop target (set up once, outside setupDrag to avoid duplicates)
(function() {
const trashBtn = document.getElementById('trashBtn');
if (trashBtn) {
trashBtn.addEventListener('dragover', e => { if (!showTrash) { e.preventDefault(); trashBtn.classList.add('drag-over'); } });
trashBtn.addEventListener('dragleave', () => trashBtn.classList.remove('drag-over'));
trashBtn.addEventListener('drop', async function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const id = parseInt(e.dataTransfer.getData('text/plain'));
if (!id) return;
const ids = selectedIds.has(id) && selectedIds.size > 1 ? [...selectedIds] : [id];
for (const sid of ids) await delEntry(sid);
clearSelection();
});
}
})();
// Document mousedown: rect selection on vault background, clear outside vault
document.addEventListener('mousedown', function(e) {
if (e.button !== 0 || rectState.active) return;
if (e.target?.closest?.('.entry-card,.entry-row,.entry-compact,.table-row-drag,#batchBar,.custom-modal-overlay.show,.edit-modal.show,.modal-overlay.show,#genModal,#settingsMenu')) return;
if (e.target?.closest?.('button,input,select,.folders-bar,.toolbar,#trashActions,.settings-dropdown,.fab,.view-toggle,.auth-section')) {
if (selectedIds.size > 0) clearSelection();
return;
}
if (e.target?.closest?.('.vault')) {
rectState.active = true;
rectState.startX = e.clientX;
rectState.startY = e.clientY;
rectState.started = false;
rectState.el = null;
selectedIds.clear();
lastSelectedId = null;
hideBatchBar();
document.getElementById('entriesContainer')?.querySelectorAll('.selected').forEach(el => el.classList.remove('selected'));
} else if (selectedIds.size > 0) {
clearSelection();
}
});
document.addEventListener('mousemove', function(e) {
if (!rectState.active) return;
const dx = e.clientX - rectState.startX;
const dy = e.clientY - rectState.startY;
if (!rectState.started && (dx > 5 || dx < -5 || dy > 5 || dy < -5)) {
rectState.started = true;
rectState.el = document.createElement('div');
rectState.el.id = 'rectSelect';
document.body.appendChild(rectState.el);
}
if (rectState.el) {
const x = Math.min(rectState.startX, e.clientX);
const y = Math.min(rectState.startY, e.clientY);
const w = Math.abs(dx);
const h = Math.abs(dy);
rectState.el.style.cssText = `left:${x}px;top:${y}px;width:${w}px;height:${h}px;display:block;position:fixed;pointer-events:none;z-index:999;border:1px solid var(--accent);background:rgba(59,130,246,0.1);`;
}
});
document.addEventListener('mouseup', function(e) {
if (!rectState.active) return;
rectState.active = false;
if (rectState.el) {
rectState.el.remove();
rectState.el = null;
if (rectState.started) {
const r = {
left: Math.min(rectState.startX, e.clientX),
top: Math.min(rectState.startY, e.clientY),
right: Math.max(rectState.startX, e.clientX),
bottom: Math.max(rectState.startY, e.clientY)
};
const container = document.getElementById('entriesContainer');
if (container) {
container.querySelectorAll('.entry-card,.entry-row,.entry-compact,.table-row-drag').forEach(el => {
const er = el.getBoundingClientRect();
if (er.left < r.right && er.right > r.left && er.top < r.bottom && er.bottom > r.top) {
const id = parseInt(el.dataset.id);
if (id) selectedIds.add(id);
}
});
}
if (selectedIds.size > 0) { updateBatchBar(); render(); }
}
}
});
document.addEventListener('keypress', e => { if (e.key === 'Enter') { if (document.getElementById('passwordInput') === document.activeElement) addEntry(); else if (document.getElementById('loginPassword') === document.activeElement) login(); else if (document.getElementById('regPassword') === document.activeElement) register(); } });
document.getElementById('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
// ==================== KEYBOARD SHORTCUTS ====================
document.addEventListener('keydown', function(e) {
const tag = document.activeElement?.tagName;
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
// Escape – close any open modal or settings
if (e.key === 'Escape') {
if (rectState.active || rectState.el) { rectState.active = false; if (rectState.el) { rectState.el.remove(); rectState.el = null; } clearSelection(); return; }
if (selectedIds.size > 0) { clearSelection(); return; }
if (!document.getElementById('settingsMenu').classList.contains('hidden')) {
document.getElementById('settingsMenu').classList.add('hidden');
return;
}
if (document.getElementById('addModal').classList.contains('show')) { closeAdd(); return; }
if (document.getElementById('editModal').classList.contains('show')) { closeEdit(); return; }
if (document.getElementById('genModal').style.display === 'flex') { closeGen(); return; }
const confirm = document.querySelector('.custom-confirm.show');
if (confirm) confirm.remove();
return;
}
// ? or / to show shortcuts help (only in vault)
if ((e.key === '?' || e.key === '/') && !isInput) {
if (!document.getElementById('authSection').classList.contains('hidden')) return;
e.preventDefault();
showShortcutsHelp();
return;
}
// Only handle Ctrl+[key], no Shift/Alt/Meta
if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
// Prevent browser defaults for ALL our shortcuts BEFORE dispatching
const code = e.code;
if (code === 'KeyN' || code === 'KeyF' || code === 'KeyT' || code === 'KeyL' || code === 'KeyS') {
e.preventDefault();
}
if (code === 'KeyN') {
if (!isInput && !document.getElementById('addModal').classList.contains('show')) openAdd();
} else if (code === 'KeyF') {
const el = document.getElementById('searchInput');
if (el) { el.focus(); el.select(); }
} else if (code === 'KeyT') {
if (!isInput) toggleTrash();
} else if (code === 'KeyL') {
if (!isInput) doLogout();
} else if (code === 'KeyS') {
if (document.getElementById('addModal').classList.contains('show')) addEntry();
else if (document.getElementById('editModal').classList.contains('show')) saveEdit();
}
});