feat: integrate Tagify for multi-tag support and add a new view-note modal across the application
This commit is contained in:
@@ -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 ────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user