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 = `

📁 New Folder

`; 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 = `

🔑 One more step

Enter your master password to unlock the vault

`; 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 = '' + (showMail ? '' : '') + '' + (!showTrash ? '' : '') + ''; filtered.forEach(e => { const sel = selectedIds.has(e.id); h += '' + '' + '' + (showMail ? '' : '') + ''; if (!showTrash) { h += '' + ''; } else { h += '' + ''; } h += ''; }); h += '
SiteUserPasswordFolderDeletedActions
' + (e.favorite ? '⭐' : '') + '🌐 ' + highlightText(e.site, searchQuery) + '👤 ' + highlightText(e.username, searchQuery) + '••••••••📁 ' + esc(e.folder || 'All') + '' + ' ' + ' ' + ' ' + '' + '🗑️ ' + timeAgo(e.deleted_at) + '' + ' ' + '' + '
'; 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 = 90; cv.height = 28; const g = cv.getContext('2d'); g.fillStyle = 'rgba(0,0,0,0.75)'; g.beginPath(); g.moveTo(14,0); g.lineTo(76,0); g.quadraticCurveTo(90,0,90,14); g.quadraticCurveTo(90,28,76,28); g.lineTo(14,28); g.quadraticCurveTo(0,28,0,14); g.quadraticCurveTo(0,0,14,0); g.fill(); g.fillStyle = '#3b82f6'; g.beginPath(); g.arc(14,14,10,0,Math.PI*2); g.fill(); g.fillStyle = '#fff'; g.font = 'bold 13px sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(''+dragIds.length,14,15); g.fillStyle = '#fff'; g.font = '12px sans-serif'; g.textAlign = 'left'; g.fillText('selected',28,15); cv.style.position = 'fixed'; cv.style.top = '-1000px'; document.body.appendChild(cv); e.dataTransfer.setDragImage(cv, 6, 14); 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 = `

📤 Export Passwords

Re-enter master password to export plaintext passwords

`; 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(); } });