add delete confirm, cancel shortcut + batch operations
This commit is contained in:
@@ -214,4 +214,65 @@ input:focus, select:focus { border-color:var(--accent); }
|
|||||||
#entriesContainer.grid-view { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
#entriesContainer.grid-view { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
|
||||||
.toolbar { flex-direction:column; }
|
.toolbar { flex-direction:column; }
|
||||||
.folders-bar { flex-direction:column; align-items:stretch; }
|
.folders-bar { flex-direction:column; align-items:stretch; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating action button */
|
||||||
|
.fab {
|
||||||
|
position: fixed; bottom: 2rem; right: 2rem;
|
||||||
|
width: 56px; height: 56px; border-radius: 50%;
|
||||||
|
background: var(--accent); color: #fff; border: none;
|
||||||
|
font-size: 1.8rem; cursor: pointer; box-shadow: 0 8px 20px rgba(0,0,0,0.4);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: 0.2s; z-index: 100;
|
||||||
|
}
|
||||||
|
.fab:hover { transform: scale(1.1); filter: brightness(1.1); }
|
||||||
|
|
||||||
|
/* Generic modal overlay (if you don’t already have it) */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.7); display: none;
|
||||||
|
justify-content: center; align-items: center; z-index: 1001;
|
||||||
|
}
|
||||||
|
.modal-overlay.show { display: flex; }
|
||||||
|
|
||||||
|
/* Modal box (used by both edit and add modals) */
|
||||||
|
.modal-box {
|
||||||
|
background: var(--bg2); border-radius: 1.5rem; padding: 1.5rem;
|
||||||
|
min-width: 380px; max-width: 90%;
|
||||||
|
}
|
||||||
|
.modal-box h3 { margin-bottom: 1rem; color: var(--text); }
|
||||||
|
.modal-box label { color: var(--text2); font-size: 0.8rem; display: block; margin-bottom: 0.2rem; }
|
||||||
|
.modal-box input, .modal-box select { width: 100%; margin-bottom: 0.5rem; }
|
||||||
|
/* Batch select */
|
||||||
|
.select-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
z-index: 2;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
.batch-actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 2rem;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.batch-actions button {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.35rem 0.8rem;
|
||||||
|
}
|
||||||
|
.selected-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
+35
-12
@@ -33,7 +33,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Add Password Modal -->
|
||||||
|
<div class="modal-overlay" id="addModal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3>➕ New Password</h3>
|
||||||
|
<label>Website / App</label><input type="text" id="addSite" placeholder="example.com">
|
||||||
|
<label style="margin-top:.6rem">Username / Email (optional)</label><input type="text" id="addUsername" placeholder="user@example.com">
|
||||||
|
<label style="margin-top:.6rem">Password</label>
|
||||||
|
<div style="position:relative">
|
||||||
|
<input type="password" id="addPassword" placeholder="Password" style="width:100%;padding-right:40px" oninput="checkAddStrength()">
|
||||||
|
<button type="button" onclick="openGen()" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;font-size:1.1rem;line-height:1" title="Generate">🎲</button>
|
||||||
|
</div>
|
||||||
|
<div class="strength-bar s0" id="addStrengthBar" style="margin-top:0.2rem;"></div>
|
||||||
|
<label style="margin-top:.6rem">Folder</label>
|
||||||
|
<select id="addFolder" style="width:100%;margin-bottom:.4rem"></select>
|
||||||
|
<div style="display:flex;gap:.5rem;margin-top:1rem">
|
||||||
|
<button class="btn" id="addEntryBtn" onclick="addEntry()">💾 Save</button>
|
||||||
|
<button class="btn btn-outline" onclick="closeAdd()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Floating action button -->
|
||||||
|
<button class="fab" onclick="openAdd()" title="Add password">+</button>
|
||||||
<div class="vault">
|
<div class="vault">
|
||||||
<h1>🔐 Vault <span>XAMPP</span>
|
<h1>🔐 Vault <span>XAMPP</span>
|
||||||
<button class="btn btn-outline btn-xs" onclick="toggleTheme()" style="margin-left:auto">🌓</button>
|
<button class="btn btn-outline btn-xs" onclick="toggleTheme()" style="margin-left:auto">🌓</button>
|
||||||
@@ -106,17 +128,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Entry Form -->
|
<!-- Add Entry Form -->
|
||||||
<form onsubmit="return false" class="input-group" autocomplete="off">
|
<!-- <form onsubmit="return false" class="input-group" autocomplete="off"> -->
|
||||||
<input type="text" id="siteInput" placeholder="Website *" autocomplete="off">
|
<!-- <input type="text" id="siteInput" placeholder="Website *" autocomplete="off"> -->
|
||||||
<input type="text" id="usernameInput" placeholder="Username (optional)" autocomplete="off">
|
<!-- <input type="text" id="usernameInput" placeholder="Username (optional)" autocomplete="off"> -->
|
||||||
<div style="flex:1;min-width:130px;position:relative">
|
<!-- <div style="flex:1;min-width:130px;position:relative"> -->
|
||||||
<input type="password" id="passwordInput" placeholder="Password *" autocomplete="new-password" style="width:100%;padding-right:40px" oninput="checkStrength()">
|
<!-- <input type="password" id="passwordInput" placeholder="Password *" autocomplete="new-password" style="width:100%;padding-right:40px" oninput="checkStrength()"> -->
|
||||||
<button type="button" onclick="openGen()" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;font-size:1.1rem;line-height:1" title="Generate password">🎲</button>
|
<!-- <button type="button" onclick="openGen()" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;font-size:1.1rem;line-height:1" title="Generate password">🎲</button> -->
|
||||||
</div>
|
<!-- </div> -->
|
||||||
<div class="strength-bar s0" id="strengthBar" style="flex-basis:100%;margin-top:-0.3rem;margin-bottom:0.3rem"></div>
|
<!-- <div class="strength-bar s0" id="strengthBar" style="flex-basis:100%;margin-top:-0.3rem;margin-bottom:0.3rem"></div> -->
|
||||||
<select id="addFolderSelect"></select>
|
<!-- <select id="addFolderSelect"></select> -->
|
||||||
<button type="button" class="btn" id="addBtn" onclick="addEntry()">➕ Add</button>
|
<!-- <button type="button" class="btn" id="addBtn" onclick="addEntry()">➕ Add</button> -->
|
||||||
</form>
|
<!-- </form> -->
|
||||||
|
|
||||||
<!-- Toolbar with search + trash + views -->
|
<!-- Toolbar with search + trash + views -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -126,6 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap">
|
<div style="display:flex;gap:.4rem;align-items:center;flex-wrap:wrap">
|
||||||
<button class="btn btn-outline btn-sm" id="trashBtn" onclick="toggleTrash()" title="Trash">🗑️</button>
|
<button class="btn btn-outline btn-sm" id="trashBtn" onclick="toggleTrash()" title="Trash">🗑️</button>
|
||||||
|
<button class="btn btn-outline btn-sm" id="selectBtn" onclick="toggleSelectMode()" title="Batch select">☐ Select</button>
|
||||||
<div class="view-toggle" id="viewToggle">
|
<div class="view-toggle" id="viewToggle">
|
||||||
<button class="view-btn active" data-view="grid">🟫 Grid</button>
|
<button class="view-btn active" data-view="grid">🟫 Grid</button>
|
||||||
<button class="view-btn" data-view="compact">📝 Compact</button>
|
<button class="view-btn" data-view="compact">📝 Compact</button>
|
||||||
@@ -149,7 +172,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Generator Modal -->
|
<!-- Generator Modal -->
|
||||||
<div id="genModal" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:1000">
|
<div id="genModal" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:2000">
|
||||||
<div style="background:var(--bg2);border-radius:1.5rem;padding:1.5rem;min-width:320px;max-width:90%">
|
<div style="background:var(--bg2);border-radius:1.5rem;padding:1.5rem;min-width:320px;max-width:90%">
|
||||||
<h3 style="margin-bottom:.8rem;color:var(--text)">🎲 Generator</h3>
|
<h3 style="margin-bottom:.8rem;color:var(--text)">🎲 Generator</h3>
|
||||||
<div style="background:var(--input);padding:.8rem;border-radius:1rem;font-family:monospace;text-align:center;color:#4ade80;margin:.6rem 0;word-break:break-all" id="genPreview">Click Generate</div>
|
<div style="background:var(--input);padding:.8rem;border-radius:1rem;font-family:monospace;text-align:center;color:#4ade80;margin:.6rem 0;word-break:break-all" id="genPreview">Click Generate</div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ let cryptoKey = null;
|
|||||||
let idleT, warnT, countT;
|
let idleT, warnT, countT;
|
||||||
let draggedId = null;
|
let draggedId = null;
|
||||||
let showTrash = false;
|
let showTrash = false;
|
||||||
|
let selectMode = false;
|
||||||
|
let selectedIds = new Set();
|
||||||
|
|
||||||
// ==================== SOUND ====================
|
// ==================== SOUND ====================
|
||||||
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
|
let soundEnabled = localStorage.getItem('soundEnabled') !== 'false';
|
||||||
@@ -125,8 +127,8 @@ function setAutoLock() { lockMin = parseInt(document.getElementById('autoLockTim
|
|||||||
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); } }
|
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 ====================
|
// ==================== USERNAME ====================
|
||||||
function saveUsername() { const f = document.getElementById('usernameInput'); if (f && f.value.trim()) localStorage.setItem('savedUsername', f.value.trim()); }
|
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('usernameInput'); if (s && f) f.value = s; }
|
function loadUsername() { const s = localStorage.getItem('savedUsername'); const f = document.getElementById('addUsername'); if (s && f) f.value = s; }
|
||||||
|
|
||||||
// ==================== FOLDERS ====================
|
// ==================== FOLDERS ====================
|
||||||
async function loadFolders() {
|
async function loadFolders() {
|
||||||
@@ -196,25 +198,13 @@ function renderFolders() {
|
|||||||
bar.innerHTML = html;
|
bar.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateAddFolderSelect() {
|
|
||||||
const select = document.getElementById('addFolderSelect');
|
|
||||||
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 selectFolder(f) {
|
function selectFolder(f) {
|
||||||
selectedFolder = f;
|
selectedFolder = f;
|
||||||
localStorage.setItem('selectedFolder', f);
|
localStorage.setItem('selectedFolder', f);
|
||||||
renderFolders();
|
renderFolders();
|
||||||
populateAddFolderSelect();
|
populateFolderSelects();
|
||||||
render();
|
render();
|
||||||
playSound('click');
|
playSound('click');
|
||||||
}
|
}
|
||||||
@@ -229,7 +219,7 @@ function showAddFolderModal() {
|
|||||||
const name = document.getElementById('newFolderName').value.trim();
|
const name = document.getElementById('newFolderName').value.trim();
|
||||||
if (!name) { toast('Enter a name', 'error'); return; }
|
if (!name) { toast('Enter a name', 'error'); return; }
|
||||||
const ok = await addFolderToServer(name);
|
const ok = await addFolderToServer(name);
|
||||||
if (ok) { renderFolders(); populateAddFolderSelect(); overlay.remove(); toast('📁 Folder created!'); playSound('success'); }
|
if (ok) { renderFolders(); populateFolderSelects(); overlay.remove(); toast('📁 Folder created!'); playSound('success'); }
|
||||||
};
|
};
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||||
playSound('open');
|
playSound('open');
|
||||||
@@ -243,7 +233,7 @@ function showDeleteFolderConfirm(folderName) {
|
|||||||
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
|
document.getElementById('cancelDeleteFolder').onclick = () => overlay.remove();
|
||||||
document.getElementById('confirmDeleteFolder').onclick = async () => {
|
document.getElementById('confirmDeleteFolder').onclick = async () => {
|
||||||
const ok = await deleteFolderFromServer(folderName);
|
const ok = await deleteFolderFromServer(folderName);
|
||||||
if (ok) { renderFolders(); populateAddFolderSelect(); render(); overlay.remove(); toast('📁 Folder deleted'); playSound('delete'); }
|
if (ok) { renderFolders(); populateFolderSelects(); render(); overlay.remove(); toast('📁 Folder deleted'); playSound('delete'); }
|
||||||
};
|
};
|
||||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||||
}
|
}
|
||||||
@@ -293,7 +283,7 @@ function init() {
|
|||||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
||||||
document.getElementById('autoLockTimer').value = lockMin;
|
document.getElementById('autoLockTimer').value = lockMin;
|
||||||
applyTheme();
|
applyTheme();
|
||||||
document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
// document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
||||||
loadUsername();
|
loadUsername();
|
||||||
const sl = localStorage.getItem('savedLoginUser');
|
const sl = localStorage.getItem('savedLoginUser');
|
||||||
if (sl) document.getElementById('loginUsername').value = sl;
|
if (sl) document.getElementById('loginUsername').value = sl;
|
||||||
@@ -310,6 +300,9 @@ function init() {
|
|||||||
});
|
});
|
||||||
const clearBtn = document.getElementById('clearSearchBtn');
|
const clearBtn = document.getElementById('clearSearchBtn');
|
||||||
if (clearBtn) clearBtn.style.display = 'none';
|
if (clearBtn) clearBtn.style.display = 'none';
|
||||||
|
document.getElementById('addModal').addEventListener('click', e => {
|
||||||
|
if (e.target === e.currentTarget) closeAdd();
|
||||||
|
});
|
||||||
// Close settings when clicking outside
|
// Close settings when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const menu = document.getElementById('settingsMenu');
|
const menu = document.getElementById('settingsMenu');
|
||||||
@@ -321,15 +314,66 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); syncSettingsUI(); render(); playSound('click'); }
|
function toggleViewBtn() { showView = !showView; localStorage.setItem('showViewBtn', showView); syncSettingsUI(); render(); playSound('click'); }
|
||||||
function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); syncSettingsUI(); document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; render(); playSound('click'); }
|
function toggleShowEmail() { showMail = !showMail; localStorage.setItem('showEmail', showMail); syncSettingsUI(); render(); playSound('click'); }
|
||||||
|
|
||||||
// ==================== GENERATOR ====================
|
// ==================== GENERATOR ====================
|
||||||
function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); }
|
function openGen() { document.getElementById('genModal').style.display = 'flex'; genPwd(); playSound('open'); }
|
||||||
function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); }
|
function closeGen() { document.getElementById('genModal').style.display = 'none'; playSound('close'); }
|
||||||
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
|
function onLenChange() { document.getElementById('lenVal').textContent = document.getElementById('pwdLen').value; genPwd(); }
|
||||||
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 = ''; for (let i = 0; i < l; i++) p += c.charAt(Math.floor(Math.random() * c.length)); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
|
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 = ''; for (let i = 0; i < l; i++) p += c.charAt(Math.floor(Math.random() * c.length)); genPwdVal = p; document.getElementById('genPreview').textContent = p; }
|
||||||
function useGen() { if (!genPwdVal) genPwd(); document.getElementById('passwordInput').value = genPwdVal; checkStrength(); navigator.clipboard.writeText(genPwdVal); toast('🎲 Copied!'); playSound('generate'); closeGen(); }
|
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 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 ====================
|
// ==================== 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'); }
|
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'); }
|
||||||
|
|
||||||
@@ -401,11 +445,11 @@ function showVault() {
|
|||||||
document.getElementById('authSection').classList.add('hidden');
|
document.getElementById('authSection').classList.add('hidden');
|
||||||
document.getElementById('vaultSection').classList.remove('hidden');
|
document.getElementById('vaultSection').classList.remove('hidden');
|
||||||
document.getElementById('currentUser').textContent = '👤 ' + curUser;
|
document.getElementById('currentUser').textContent = '👤 ' + curUser;
|
||||||
document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
//document.getElementById('usernameInput').style.display = showMail ? '' : 'none';
|
||||||
document.getElementById('autoLockTimer').value = lockMin;
|
document.getElementById('autoLockTimer').value = lockMin;
|
||||||
loadUsername();
|
loadUsername();
|
||||||
renderFolders();
|
renderFolders();
|
||||||
populateAddFolderSelect();
|
populateFolderSelects();
|
||||||
syncSettingsUI();
|
syncSettingsUI();
|
||||||
resetIdle();
|
resetIdle();
|
||||||
}
|
}
|
||||||
@@ -433,7 +477,7 @@ async function loadEntries(q) {
|
|||||||
document.getElementById('connectionStatus').textContent = '🟢 Connected';
|
document.getElementById('connectionStatus').textContent = '🟢 Connected';
|
||||||
document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)';
|
document.getElementById('entryCount').textContent = '(' + entries.length + ' entries)';
|
||||||
renderFolders();
|
renderFolders();
|
||||||
populateAddFolderSelect();
|
populateFolderSelects();
|
||||||
render();
|
render();
|
||||||
} else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); }
|
} else if (r.status === 401) { toast('Session expired', 'error'); doLogout(); }
|
||||||
} catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); }
|
} catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); }
|
||||||
@@ -461,20 +505,22 @@ function render() {
|
|||||||
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
|
(showMail ? '<td>👤 ' + esc(e.username) + '</td>' : '') +
|
||||||
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>';
|
'<td class="password-cell"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span></td>';
|
||||||
if (!showTrash) {
|
if (!showTrash) {
|
||||||
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
h += '<td><span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span></td>' +
|
||||||
'<td class="actions-cell">' +
|
'<td class="actions-cell">' +
|
||||||
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button> ' : '') +
|
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
|
||||||
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button> ' : '') +
|
||||||
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button> ' +
|
||||||
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
'<button class="edit-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✏️</button> ' +
|
||||||
'</td>';
|
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
||||||
} else {
|
'</td>';
|
||||||
h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' +
|
} else {
|
||||||
'<td class="actions-cell">' +
|
h += '<td><span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></td>' +
|
||||||
'<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️ Restore</button> ' +
|
'<td class="actions-cell">' +
|
||||||
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
(selectMode ? `<input type="checkbox" class="select-checkbox" style="position:static;margin-right:8px;" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">` : '') +
|
||||||
'</td>';
|
'<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button> ' +
|
||||||
}
|
'<button class="delete-btn" data-id="' + e.id + '" style="position:static;display:inline-flex;vertical-align:middle;">✕</button>' +
|
||||||
|
'</td>';
|
||||||
|
}
|
||||||
h += '</tr>';
|
h += '</tr>';
|
||||||
});
|
});
|
||||||
h += '</tbody></table>';
|
h += '</tbody></table>';
|
||||||
@@ -491,16 +537,85 @@ function render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gridC(e) {
|
function gridC(e) {
|
||||||
if (showTrash) return '<div class="entry-card" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="trash-info">🗑️ ' + timeAgo(e.deleted_at) + '</div></div>';
|
let html = '<div class="entry-card" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||||
return '<div class="entry-card" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="card-site">🌐 ' + esc(e.site) + '</div>' + (showMail ? '<div class="card-user">👤 ' + esc(e.username) + '</div>' : '') + '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div><div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span><div>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div></div>';
|
if (selectMode) {
|
||||||
|
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||||
|
}
|
||||||
|
html += '<div class="action-btns">';
|
||||||
|
if (showTrash) {
|
||||||
|
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="edit-btn" data-id="' + e.id + '">✏️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="card-site">🌐 ' + esc(e.site) + '</div>';
|
||||||
|
if (showMail) html += '<div class="card-user">👤 ' + esc(e.username) + '</div>';
|
||||||
|
html += '<div class="card-folder">📁 ' + esc(e.folder || 'All') + '</div>';
|
||||||
|
if (!showTrash) {
|
||||||
|
html += '<div class="card-password"><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span><div>' +
|
||||||
|
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') +
|
||||||
|
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="trash-info">🗑️ ' + timeAgo(e.deleted_at) + '</div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
function listC(e) {
|
function listC(e) {
|
||||||
if (showTrash) return '<div class="entry-row" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></div></div>';
|
let html = '<div class="entry-row" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||||
return '<div class="entry-row" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="entry-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div></div></div>';
|
if (selectMode) {
|
||||||
|
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||||
|
}
|
||||||
|
html += '<div class="action-btns">';
|
||||||
|
if (showTrash) {
|
||||||
|
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="edit-btn" data-id="' + e.id + '">✏️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="entry-info"><span class="entry-site">🌐 ' + esc(e.site) + '</span>';
|
||||||
|
if (showMail) html += '<span class="entry-user">👤 ' + esc(e.username) + '</span>';
|
||||||
|
if (!showTrash) {
|
||||||
|
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>';
|
||||||
|
html += '<div class="password-field"><span class="password-text" id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' +
|
||||||
|
(showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') +
|
||||||
|
'<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span>';
|
||||||
|
}
|
||||||
|
html += '</div></div>';
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
function compC(e) {
|
function compC(e) {
|
||||||
if (showTrash) return '<div class="entry-compact" data-id="' + e.id + '"><div class="action-btns"><button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span class="compact-site">🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span class="compact-user">👤 ' + esc(e.username) + '</span>' : '') + '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span></div>';
|
let html = '<div class="entry-compact" draggable="' + (!selectMode) + '" data-id="' + e.id + '">';
|
||||||
return '<div class="entry-compact" draggable="true" data-id="' + e.id + '"><div class="action-btns"><button class="edit-btn" data-id="' + e.id + '">✏️</button><button class="delete-btn" data-id="' + e.id + '">✕</button></div><span>🌐 ' + esc(e.site) + '</span>' + (showMail ? '<span>👤 ' + esc(e.username) + '</span>' : '') + '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span><span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>' + (showView ? '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>' : '') + '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button></div>';
|
if (selectMode) {
|
||||||
|
html += `<input type="checkbox" class="select-checkbox" ${selectedIds.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation();toggleSelectEntry(${e.id})">`;
|
||||||
|
}
|
||||||
|
html += '<div class="action-btns">';
|
||||||
|
if (showTrash) {
|
||||||
|
html += '<button class="restore-btn" onclick="restoreEntry(' + e.id + ')">↩️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
} else {
|
||||||
|
html += '<button class="edit-btn" data-id="' + e.id + '">✏️</button>';
|
||||||
|
html += '<button class="delete-btn" data-id="' + e.id + '">✕</button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '<span>🌐 ' + esc(e.site) + '</span>';
|
||||||
|
if (showMail) html += '<span>👤 ' + esc(e.username) + '</span>';
|
||||||
|
if (!showTrash) {
|
||||||
|
html += '<span class="entry-folder">📁 ' + esc(e.folder || 'All') + '</span>';
|
||||||
|
html += '<span id="p-' + e.id + '" data-pw="' + esc(e.password) + '">••••••••</span>';
|
||||||
|
if (showView) html += '<button class="icon-btn toggle-p" data-id="' + e.id + '">👁️</button>';
|
||||||
|
html += '<button class="icon-btn copy-p" data-id="' + e.id + '">📋</button>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="trash-badge">🗑️ ' + timeAgo(e.deleted_at) + '</span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== EVENTS ====================
|
// ==================== EVENTS ====================
|
||||||
@@ -508,18 +623,66 @@ function showConfirm(btn, message, callback) {
|
|||||||
const id = btn.dataset.id;
|
const id = btn.dataset.id;
|
||||||
const existing = document.querySelector('.custom-confirm');
|
const existing = document.querySelector('.custom-confirm');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
const confirm = document.createElement('div');
|
const confirm = document.createElement('div');
|
||||||
confirm.className = 'custom-confirm show';
|
confirm.className = 'custom-confirm show';
|
||||||
confirm.innerHTML = '<div class="confirm-text">' + message + '</div><div class="confirm-btns"><button class="confirm-yes">Yes</button><button class="confirm-no">No</button></div>';
|
confirm.innerHTML =
|
||||||
|
'<div class="confirm-text">' + message + '</div>' +
|
||||||
|
'<div class="confirm-btns">' +
|
||||||
|
'<button class="confirm-yes">Yes</button>' +
|
||||||
|
'<button class="confirm-no">No</button>' +
|
||||||
|
'</div>';
|
||||||
document.body.appendChild(confirm);
|
document.body.appendChild(confirm);
|
||||||
|
|
||||||
const rect = btn.getBoundingClientRect();
|
const rect = btn.getBoundingClientRect();
|
||||||
confirm.style.top = (rect.top - 60) + 'px';
|
confirm.style.top = (rect.top - 60) + 'px';
|
||||||
let leftPos = rect.left - confirm.offsetWidth + rect.width;
|
let leftPos = rect.left - confirm.offsetWidth + rect.width;
|
||||||
if (leftPos < 10) leftPos = 10;
|
if (leftPos < 10) leftPos = 10;
|
||||||
confirm.style.left = leftPos + 'px';
|
confirm.style.left = leftPos + 'px';
|
||||||
confirm.querySelector('.confirm-yes').onclick = () => { confirm.remove(); callback(id); showZigzagToast(btn, '🗑️ Deleted!', 'error'); playSound('delete'); };
|
|
||||||
confirm.querySelector('.confirm-no').onclick = () => confirm.remove();
|
const yesBtn = confirm.querySelector('.confirm-yes');
|
||||||
setTimeout(() => { document.addEventListener('click', function closeConfirm(e) { if (!confirm.contains(e.target) && e.target !== btn) { confirm.remove(); document.removeEventListener('click', closeConfirm); } }); }, 10);
|
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 attachEvents() {
|
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('.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)); } });
|
||||||
@@ -577,23 +740,128 @@ async function saveEdit() {
|
|||||||
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
||||||
} catch (e) { toast('⚠️ Error', 'error'); }
|
} catch (e) { toast('⚠️ Error', 'error'); }
|
||||||
}
|
}
|
||||||
|
//======================== batch operations ==========================
|
||||||
|
function toggleSelectMode() {
|
||||||
|
selectMode = !selectMode;
|
||||||
|
document.getElementById('selectBtn').textContent = selectMode ? '☑ Deselect' : '☐ Select';
|
||||||
|
document.getElementById('selectBtn').classList.toggle('active', selectMode);
|
||||||
|
if (!selectMode) {
|
||||||
|
selectedIds.clear();
|
||||||
|
hideBatchBar();
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectEntry(id) {
|
||||||
|
if (selectedIds.has(id)) {
|
||||||
|
selectedIds.delete(id);
|
||||||
|
} else {
|
||||||
|
selectedIds.add(id);
|
||||||
|
}
|
||||||
|
updateBatchBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<span class="selected-count">${selectedIds.size} selected</span>`;
|
||||||
|
|
||||||
|
if (showTrash) {
|
||||||
|
bar.innerHTML += `
|
||||||
|
<button class="btn btn-sm restore-btn" onclick="batchRestore()">↩️ Restore All</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="batchPermanentDelete()">🗑️ Delete</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
bar.innerHTML += `
|
||||||
|
<select id="batchFolder" style="font-size:0.75rem;padding:0.2rem 0.5rem;">
|
||||||
|
${folders.map(f => `<option value="${f}">📁 ${f}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm" onclick="batchMove()">Move</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="batchDelete()">Delete</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
bar.innerHTML += `<button class="btn btn-sm btn-outline" onclick="toggleSelectMode()">Cancel</button>`;
|
||||||
|
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);
|
||||||
|
selectedIds.clear();
|
||||||
|
hideBatchBar();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchPermanentDelete() {
|
||||||
|
if (!confirm(`Permanently delete ${selectedIds.size} entries?`)) return;
|
||||||
|
for (const id of selectedIds) await permanentDelete(id);
|
||||||
|
selectedIds.clear();
|
||||||
|
hideBatchBar();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchRestore() {
|
||||||
|
for (const id of selectedIds) await restoreEntry(id);
|
||||||
|
selectedIds.clear();
|
||||||
|
hideBatchBar();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
await fetch(API + '/entries/' + id, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ site: e.site, username: e.username, encrypted_password: await encryptPwd(e.password), iv: 'batch', folder })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedIds.clear();
|
||||||
|
hideBatchBar();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
// ==================== ADD / DELETE ====================
|
// ==================== ADD / DELETE ====================
|
||||||
async function addEntry() {
|
async function addEntry() {
|
||||||
const site = document.getElementById('siteInput').value.trim();
|
const site = document.getElementById('addSite').value.trim();
|
||||||
const user = document.getElementById('usernameInput').value.trim();
|
const user = document.getElementById('addUsername').value.trim();
|
||||||
const pass = document.getElementById('passwordInput').value;
|
const pass = document.getElementById('addPassword').value;
|
||||||
if (!site || !pass) { toast('❌ Site and password required', 'error'); return; }
|
if (!site || !pass) { toast('❌ Site and password required', 'error'); return; }
|
||||||
if (user) localStorage.setItem('savedUsername', user);
|
if (user) localStorage.setItem('savedUsername', user);
|
||||||
document.getElementById('addBtn').disabled = true;
|
const btn = document.getElementById('addEntryBtn');
|
||||||
try {
|
btn.disabled = true;
|
||||||
const enc = await encryptPwd(pass);
|
try {
|
||||||
const folder = document.getElementById('addFolderSelect').value;
|
const enc = await encryptPwd(pass);
|
||||||
const r = await fetch(API + '/entries', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ site, username: user, encrypted_password: enc.encrypted, iv: enc.iv, encryption_method: 'client', folder }) });
|
const folder = document.getElementById('addFolder').value;
|
||||||
if (r.ok) { document.getElementById('siteInput').value = ''; document.getElementById('passwordInput').value = ''; document.getElementById('strengthBar').className = 'strength-bar s0'; toast('✅ Saved!'); loadEntries(); playSound('success'); }
|
const r = await fetch(API + '/entries', {
|
||||||
else { const d = await r.json(); toast('❌ ' + (d.error || 'Failed'), 'error'); }
|
method: 'POST',
|
||||||
} catch (e) { toast('⚠️ Error', 'error'); }
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
finally { document.getElementById('addBtn').disabled = false; }
|
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) {
|
async function delEntry(id) {
|
||||||
try {
|
try {
|
||||||
@@ -648,4 +916,55 @@ if (token && curUser) {
|
|||||||
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
|
['click', 'keypress', 'scroll', 'mousemove'].forEach(e => document.addEventListener(e, () => { if (token) resetIdle(); }));
|
||||||
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.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('genModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGen(); });
|
||||||
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
|
document.getElementById('editModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEdit(); });
|
||||||
|
// ==================== KEYBOARD SHORTCUTS (override browser) ====================
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
// Don't fire when typing in inputs / textareas / selects
|
||||||
|
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 (!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle Ctrl+key without Shift, Alt, etc.
|
||||||
|
if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
||||||
|
|
||||||
|
if (e.key === 'n' || e.key === 'N') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isInput && !document.getElementById('addModal').classList.contains('show')) {
|
||||||
|
openAdd();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'f' || e.key === 'F') {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
searchInput.select();
|
||||||
|
}
|
||||||
|
} else if (e.key === 't' || e.key === 'T') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isInput) toggleTrash();
|
||||||
|
} else if (e.key === 'l' || e.key === 'L') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isInput) doLogout();
|
||||||
|
} else if (e.key === 's' || e.key === 'S') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Save if add/edit modal is open
|
||||||
|
if (document.getElementById('addModal').classList.contains('show')) {
|
||||||
|
addEntry();
|
||||||
|
} else if (document.getElementById('editModal').classList.contains('show')) {
|
||||||
|
saveEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user