diff --git a/api.php b/api.php index 8a097b9..7bef32e 100644 --- a/api.php +++ b/api.php @@ -88,6 +88,7 @@ try { $db->exec("ALTER TABLE vault_entries ADD COLUMN deleted_at DATETIME"); } c try { $db->exec("ALTER TABLE users DROP COLUMN encryption_key"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE users ADD COLUMN hash_algo TEXT DEFAULT 'pbkdf2'"); } catch (Exception $e) {} try { $db->exec("ALTER TABLE sessions ADD COLUMN csrf_token TEXT"); } catch (Exception $e) {} +try { $db->exec("ALTER TABLE vault_entries ADD COLUMN favorite INTEGER DEFAULT 0"); } catch (Exception $e) {} $db->exec("DELETE FROM sessions WHERE expires_at < datetime('now')"); $db->exec("DELETE FROM login_attempts WHERE attempted_at < datetime('now', '-15 minutes')"); @@ -321,6 +322,7 @@ try { 'folder' => $r['folder'] ?? 'All', 'deleted' => $r['deleted'], 'deleted_at' => $r['deleted_at'], + 'favorite' => (int)($r['favorite'] ?? 0), 'created_at' => $r['created_at'], 'updated_at' => $r['updated_at'] ]; @@ -405,6 +407,18 @@ try { echo json_encode(['message'=>'Restored']); break; + // Favorite toggle + case (preg_match('/^\/entries\/(\d+)\/favorite$/', $path, $m) && $method === 'POST'): + $auth = authenticate($db); + requireCSRF($db, $auth['userId']); + $st = $db->prepare('UPDATE vault_entries SET favorite = CASE WHEN favorite=1 THEN 0 ELSE 1 END WHERE id=:id AND user_id=:uid'); + $st->bindValue(':id', $m[1], SQLITE3_INTEGER); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->execute(); + logAudit($db, $auth['userId'], 'toggle_favorite'); + echo json_encode(['message'=>'Toggled']); + break; + case ($path === '/logout' && $method === 'POST'): $auth = authenticate($db); requireCSRF($db, $auth['userId']); diff --git a/css/style.css b/css/style.css index 64aa8df..5b30a37 100644 --- a/css/style.css +++ b/css/style.css @@ -117,6 +117,8 @@ input:focus, select:focus { border-color:var(--accent); } .folder-chip { background:var(--bg2); border:1px solid var(--border); color:var(--text2); padding:0.3rem 0.8rem; border-radius:2rem; cursor:pointer; font-size:0.78rem; transition:0.2s; white-space:nowrap; } .folder-chip:hover { background:var(--chip-color, var(--accent)); color:#fff; border-color:var(--chip-color, var(--accent)); } .folder-chip.active { background:var(--chip-color, var(--accent)); color:#fff; border-color:var(--chip-color, var(--accent)); } +.folder-chip.drag-over { border-color:var(--accent)!important; box-shadow:0 0 12px rgba(59,130,246,0.5); transform:scale(1.05); } +.pw-display.pw-hover { cursor:pointer; } .folder-count { background:rgba(0,0,0,0.3); padding:0.1rem 0.4rem; border-radius:1rem; margin-left:0.3rem; font-size:0.7rem; } .folder-delete-btn { background:transparent; border:none; color:var(--danger); cursor:pointer; font-size:0.7rem; margin-left:0.2rem; opacity:0.7; } .folder-delete-btn:hover { opacity:1; } @@ -151,6 +153,12 @@ input:focus, select:focus { border-color:var(--accent); } .edit-btn { width:20px; height:20px; background:transparent; color:var(--accent); border:none; cursor:pointer; font-size:0.75rem; display:flex; align-items:center; justify-content:center; } .edit-btn:hover { color:#fff; transform:scale(1.2); } .light .edit-btn:hover { color:#000!important; } +.star-btn { width:20px; height:20px; background:transparent; border:none; cursor:pointer; font-size:0.75rem; display:flex; align-items:center; justify-content:center; } +.star-btn:hover { transform:scale(1.3); } +.entry-card.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.entry-row.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.entry-compact.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } +.table-row-drag.favorite td { background:rgba(255,200,0,0.05); } .entry-info { display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap; flex:1; } .entry-site { font-weight:700; color:var(--text); } .entry-user { color:var(--text2); } diff --git a/index.html b/index.html index 78555f7..baa2131 100644 --- a/index.html +++ b/index.html @@ -189,7 +189,12 @@ -
| Site | ' + (showMail ? 'User | ' : '') + 'Password | ' + (!showTrash ? 'Folder | ' : 'Deleted | ') + 'Actions |
|---|
| Site | ' + (showMail ? 'User | ' : '') + 'Password | ' + (!showTrash ? 'Folder | ' : 'Deleted | ') + 'Actions | |
|---|---|---|---|---|---|---|
| ' + (e.favorite ? '⭐' : '') + ' | ' + '🌐 ' + highlightText(e.site, searchQuery) + ' | ' + (showMail ? '👤 ' + highlightText(e.username, searchQuery) + ' | ' : '') + - '•••••••• | '; + '•••••••• | '; if (!showTrash) { h += '📁 ' + esc(e.folder || 'All') + ' | ' + '' +
(selectMode ? `` : '') +
- (showView ? ' ' : '') +
+ ' ' +
' ' +
' ' +
'' +
@@ -563,7 +603,7 @@ function render() {
}
function gridC(e) {
- let html = ' ';
+ let html = ' ';
if (selectMode) {
html += ``;
}
@@ -572,6 +612,7 @@ function gridC(e) {
html += '';
html += '';
} else {
+ html += '';
html += '';
html += '';
}
@@ -580,8 +621,7 @@ function gridC(e) {
if (showMail) html += ' 👤 ' + highlightText(e.username, searchQuery) + ' ';
html += '📁 ' + esc(e.folder || 'All') + ' ';
if (!showTrash) {
- html += '•••••••• ' +
- (showView ? '' : '') +
+ html += ' •••••••• ';
} else {
html += '' +
' 🗑️ ' + timeAgo(e.deleted_at) + ' ';
@@ -590,7 +630,7 @@ function gridC(e) {
return html;
}
function listC(e) {
- let html = '';
+ let html = ' ';
if (selectMode) {
html += ``;
}
@@ -599,6 +639,7 @@ function listC(e) {
html += '';
html += '';
} else {
+ html += '';
html += '';
html += '';
}
@@ -607,8 +648,7 @@ function listC(e) {
if (showMail) html += '👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '📁 ' + esc(e.folder || 'All') + '';
- html += ' ••••••••' +
- (showView ? '' : '') +
+ html += ' ••••••••' +
' ';
} else {
html += '🗑️ ' + timeAgo(e.deleted_at) + '';
@@ -617,7 +657,7 @@ function listC(e) {
return html;
}
function compC(e) {
- let html = '';
+ let html = ' ';
if (selectMode) {
html += ``;
}
@@ -626,6 +666,7 @@ function compC(e) {
html += '';
html += '';
} else {
+ html += '';
html += '';
html += '';
}
@@ -634,8 +675,7 @@ function compC(e) {
if (showMail) html += '👤 ' + highlightText(e.username, searchQuery) + '';
if (!showTrash) {
html += '📁 ' + esc(e.folder || 'All') + '';
- html += '••••••••';
- if (showView) html += '';
+ html += '••••••••';
html += '';
} else {
html += '🗑️ ' + timeAgo(e.deleted_at) + '';
@@ -714,8 +754,20 @@ function entryPw(id) { const e = entries.find(x => x.id == id); return e ? e.pas
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('.toggle-p').forEach(b => b.onclick = function(ev) { ev.stopPropagation(); const el = document.getElementById('p-' + this.dataset.id); const pw = entryPw(this.dataset.id); el.textContent = el.textContent === '••••••••' ? pw : '••••••••'; });
+ 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;
|