feat: integrate Tagify for multi-tag support and add a new view-note modal across the application

This commit is contained in:
root
2026-03-29 16:25:45 +07:00
parent f4f842d658
commit 8dedb57692
8 changed files with 408 additions and 106 deletions

View File

@@ -1,6 +1,7 @@
<?php
// ── DB migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN folder_id INTEGER DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN tags TEXT DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS password_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -25,15 +26,15 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
}
if(isset($_POST['create_password'])) {
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id, folder_id) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null]);
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id, folder_id, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? '')]);
}
if(isset($_POST['update_password'])) {
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$_POST['password_id']])->fetch();
if($p && $p['owner_id'] == $user_id) {
$stmt = $pdo->prepare("UPDATE passwords SET title=?, username=?, password=?, url=?, notes=?, folder_id=? WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, $_POST['password_id'], $user_id]);
$stmt = $pdo->prepare("UPDATE passwords SET title=?, username=?, password=?, url=?, notes=?, folder_id=?, tags=? WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? ''), $_POST['password_id'], $user_id]);
}
}
@@ -144,6 +145,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
</div>
</div>
<div id="pw-view-notes" style="font-size:0.8rem; color:var(--text-light); margin-top:0.5rem; white-space:pre-wrap;"></div>
<div id="pw-view-tags-container" style="display:none; flex-wrap:wrap; gap:0.25rem; margin-top:1rem; padding-top:0.5rem; border-top:1px dashed var(--border);"></div>
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1rem;">
<button class="btn btn-secondary" onclick="document.getElementById('password-view-modal').classList.remove('active')">Close</button>
</div>
@@ -194,6 +196,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
<label>Notes</label>
<textarea name="notes" id="pw-edit-notes" class="form-control" rows="2" placeholder="Optional notes..."></textarea>
</div>
<div class="form-group">
<label>Tags</label>
<input type="text" name="tags" id="pw-edit-tags" class="form-control tagify-input" placeholder="Add tags...">
</div>
<!-- Shares management section (populated by JS) -->
<div id="pw-shares-section" style="display:none; border-top:1px solid var(--border); padding-top:0.75rem; margin-top:0.5rem;">
<label style="font-size:0.8rem; color:var(--text-light); display:block; margin-bottom:0.4rem;">Shared with</label>
@@ -298,7 +304,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
<?php endforeach; ?>
</select>
<button type="submit" name="bulk_action" value="move_folder" class="btn btn-sm btn-secondary"><i class="fa fa-folder-arrow-down"></i> Move</button>
<button type="submit" name="bulk_action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete selected entries?')"><i class="fa fa-trash"></i> Delete</button>
<button type="button" class="btn btn-sm btn-danger" onclick="appConfirm('Delete selected entries?').then(y => { if(y) { const el = document.createElement('input'); el.type='hidden'; el.name='bulk_action'; el.value='delete'; this.form.appendChild(el); this.form.submit(); } })"><i class="fa fa-trash"></i> Delete</button>
</div>
<!-- Entries List -->
@@ -364,6 +370,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
data-notes="<?= htmlspecialchars(addslashes((string)($p['notes'] ?? ''))) ?>"
data-folder-id="<?= htmlspecialchars((string)($p['folder_id'] ?? '')) ?>"
data-shares="<?= $shares_json ?>"
data-tags="<?= htmlspecialchars((string)($p['tags'] ?? '')) ?>"
onclick="openViewEntry(this)">
<!-- Checkbox (only for owned items) -->
<?php if($p['is_mine']): ?>
@@ -473,10 +480,33 @@ function openViewEntry(row) {
document.getElementById('pw-view-pw').value = row.dataset.pw;
document.getElementById('pw-view-pw').type = 'password';
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
const urlEl = document.getElementById('pw-view-url');
urlEl.textContent = '';
urlEl.textContent = row.dataset.url || '';
const urlEl = document.createElement('div');
document.getElementById('pw-view-url').innerHTML = ''; // Clear existing
if(row.dataset.url) {
urlEl.innerHTML = `<a href="${row.dataset.url}" target="_blank" style="color:var(--primary);">${row.dataset.url}</a>`;
document.getElementById('pw-view-url').appendChild(urlEl);
}
document.getElementById('pw-view-notes').textContent = row.dataset.notes || '';
// Tag rendering
const tagsContainer = document.getElementById('pw-view-tags-container');
tagsContainer.innerHTML = '';
const tagsStr = row.dataset.tags || '';
if(tagsStr) {
tagsContainer.style.display = 'flex';
tagsStr.split(',').forEach(t => {
t = t.trim();
if(!t) return;
const span = document.createElement('span');
span.style.cssText = 'font-size:0.65rem; padding:2px 8px; border-radius:12px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-weight:600; display:inline-flex; align-items:center; gap:0.2rem;';
span.innerHTML = `<i class="fa fa-tag" style="font-size:0.6rem;"></i> ${t}`;
tagsContainer.appendChild(span);
});
} else {
tagsContainer.style.display = 'none';
}
document.getElementById('password-view-modal').classList.add('active');
}
@@ -504,6 +534,7 @@ function openAddPassword() {
document.getElementById('pw-edit-url').value = '';
document.getElementById('pw-edit-notes').value = '';
document.getElementById('pw-edit-folder').value = '';
document.getElementById('pw-edit-tags').value = '';
document.getElementById('pw-shares-section').style.display = 'none';
document.getElementById('pw-shares-list').innerHTML = '';
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
@@ -534,6 +565,7 @@ function openEditEntry(row) {
document.getElementById('pw-edit-url').value = row.dataset.url || '';
document.getElementById('pw-edit-notes').value = row.dataset.notes || '';
document.getElementById('pw-edit-folder').value = row.dataset.folderId || '';
document.getElementById('pw-edit-tags').value = row.dataset.tags || '';
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
renderPasswordShares(row.dataset.shares || '[]');
document.getElementById('password-edit-modal').classList.add('active');
@@ -568,15 +600,21 @@ function renderPasswordShares(jsonShares) {
function removePasswordShare(shareId) {
if(!shareId) return;
if(!confirm('Unshare this password access?')) return;
document.getElementById('remove-share-id').value = String(shareId);
document.getElementById('remove-share-form').submit();
appConfirm('Unshare this password access?').then(y => {
if(y) {
document.getElementById('remove-share-id').value = String(shareId);
document.getElementById('remove-share-form').submit();
}
});
}
function deletePassword() {
if(!confirm('Delete this entry permanently?')) return;
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
document.getElementById('delete-pw-form').submit();
appConfirm('Delete this entry permanently?').then(y => {
if(y) {
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
document.getElementById('delete-pw-form').submit();
}
});
}
// ── Toggles ───────────────────────────────────────────────────────────────────
@@ -588,7 +626,8 @@ function filterVault(q) {
q = q.toLowerCase();
document.querySelectorAll('.vault-entry').forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(q) ? '' : 'none';
const tags = (row.dataset.tags || '').toLowerCase();
row.style.display = (text.includes(q) || tags.includes(q)) ? '' : 'none';
});
}
@@ -602,9 +641,12 @@ function showAddFolderPrompt() {
}
function deleteFolder(id, name) {
if(!confirm(`Delete folder "${name}"? Entries will not be deleted.`)) return;
document.getElementById('del-folder-id').value = id;
document.getElementById('del-folder-form').submit();
appConfirm(`Delete folder "${name}"? Entries will not be deleted.`).then(y => {
if(y) {
document.getElementById('del-folder-id').value = id;
document.getElementById('del-folder-form').submit();
}
});
}
// ── Hotkeys on hovered row ────────────────────────────────────────────────────