Enhance note-taking features by adding label support, improving modal styles, and refining CSS for better responsiveness. Update login page title and adjust password management UI for better usability.

This commit is contained in:
root
2026-03-27 20:04:52 +07:00
parent 937033ea07
commit 99f948ba5d
5 changed files with 188 additions and 48 deletions

View File

@@ -10,7 +10,7 @@
<body>
<div class="login-container">
<div class="login-card">
<h1>Welcome Back</h1>
<h1>Login</h1>
<?php if($error): ?><div class="alert"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="POST">
<div class="form-group">
@@ -23,7 +23,9 @@
</div>
<button type="submit" class="btn" style="width:100%;">Sign In</button>
</form>
<!--
<p style="text-align:center; margin-top:1rem; font-size:0.875rem; color:var(--text-light);">First time? Run <a href="?action=setup">setup</a> to install Database.</p>
-->
</div>
</div>
</body>

View File

@@ -3,6 +3,7 @@
try { $pdo->exec("ALTER TABLE notes ADD COLUMN color TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN image_path TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN label_name TEXT DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -40,8 +41,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) $image_path = $dest;
}
}
$stmt = $pdo->prepare("INSERT INTO notes (title, content, owner_id, color, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $user_id, $_POST['color'] ?? null, $image_path]);
$stmt = $pdo->prepare("INSERT INTO notes (title, content, owner_id, color, image_path, label_name) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $user_id, $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null]);
}
if(isset($_POST['update_note'])) {
@@ -64,8 +65,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
if($image_path && file_exists($image_path)) @unlink($image_path);
$image_path = null;
}
$stmt = $pdo->prepare("UPDATE notes SET title=?, content=?, color=?, image_path=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $_POST['color'] ?? null, $image_path, $_POST['note_id'], $user_id]);
$stmt = $pdo->prepare("UPDATE notes SET title=?, content=?, color=?, image_path=?, label_name=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null, $_POST['note_id'], $user_id]);
}
}
@@ -111,6 +112,12 @@ $notes = $stmt->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
$label_names = [];
foreach($notes as $n) {
$lbl = trim((string)($n['label_name'] ?? ''));
if($lbl !== '') $label_names[strtolower($lbl)] = $lbl;
}
natcasesort($label_names);
// Define note colors — Google Keep inspired
$note_colors = [
@@ -122,31 +129,53 @@ $note_colors = [
'#b3e5fc' => ['bg'=>'#b3e5fc', 'label'=>'Sky', 'hex'=>'#b3e5fc'],
'#cce5ff' => ['bg'=>'#cce5ff', 'label'=>'Blue', 'hex'=>'#cce5ff'],
'#e2d9f3' => ['bg'=>'#e2d9f3', 'label'=>'Lavender', 'hex'=>'#e2d9f3'],
'#f3e5f5' => ['bg'=>'#f3e5f5', 'label'=>'Orchid', 'hex'=>'#f3e5f5'],
'#f8d7da' => ['bg'=>'#f8d7da', 'label'=>'Rose', 'hex'=>'#f8d7da'],
'#ef9a9a' => ['bg'=>'#ef9a9a', 'label'=>'Red', 'hex'=>'#ef9a9a'],
'#ffcc80' => ['bg'=>'#ffcc80', 'label'=>'Peach', 'hex'=>'#ffcc80'],
'#fde8d8' => ['bg'=>'#fde8d8', 'label'=>'Orange', 'hex'=>'#fde8d8'],
'#d7ccc8' => ['bg'=>'#d7ccc8', 'label'=>'Sand', 'hex'=>'#d7ccc8'],
'#f5f5f5' => ['bg'=>'#f5f5f5', 'label'=>'Stone', 'hex'=>'#f5f5f5'],
'#e8f5e9' => ['bg'=>'#e8f5e9', 'label'=>'Mint', 'hex'=>'#e8f5e9'],
'#fff3e0' => ['bg'=>'#fff3e0', 'label'=>'Apricot', 'hex'=>'#fff3e0'],
'#fce4ec' => ['bg'=>'#fce4ec', 'label'=>'Pink', 'hex'=>'#fce4ec'],
'#ede7f6' => ['bg'=>'#ede7f6', 'label'=>'Violet', 'hex'=>'#ede7f6'],
'#e0f7fa' => ['bg'=>'#e0f7fa', 'label'=>'Cyan', 'hex'=>'#e0f7fa'],
'#fffde7' => ['bg'=>'#fffde7', 'label'=>'Butter', 'hex'=>'#fffde7'],
];
?>
<div style="display:flex; justify-content:flex-end; align-items:center; margin-bottom:1rem;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; gap:0.75rem; flex-wrap:wrap;">
<button class="btn" onclick="document.getElementById('new-note-modal').classList.add('active')">
<i class="fa fa-plus"></i> New Note
</button>
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
<input type="text" id="notes-search-input" class="form-control" placeholder="Search notes..." style="width:200px; padding:0.45rem 0.6rem; font-size:0.84rem;" oninput="filterNotes()">
<select id="notes-label-filter" class="form-control" style="width:180px; padding:0.45rem 0.6rem; font-size:0.84rem;" onchange="filterNotes()">
<option value="">All labels</option>
<?php foreach($label_names as $lbl): ?>
<option value="<?= htmlspecialchars(strtolower($lbl)) ?>"><?= htmlspecialchars($lbl) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- ── New Note Modal ─────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="new-note-modal">
<div class="modal-content" style="max-width:580px; max-height:92vh; overflow-y:auto; padding:1.5rem;">
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:92vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:700;">New Note</h3>
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.2rem;" onclick="document.getElementById('new-note-modal').classList.remove('active')">×</button>
</div>
<form method="POST" enctype="multipart/form-data" id="new-note-form">
<input type="hidden" name="create_note" value="1">
<div class="form-group">
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
<div class="form-group">
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
</div>
<div class="form-group">
<input type="text" name="label_name" class="form-control" id="new-note-label" placeholder="Tag/Label name">
</div>
</div>
<!-- WYSIWYG Toolbar -->
<div class="note-editor-toolbar" id="new-note-toolbar">
@@ -196,13 +225,21 @@ $note_colors = [
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="edit-note-modal">
<div class="modal-content" style="max-width:600px; max-height:90vh; overflow-y:auto;">
<h3 style="margin-bottom:1rem;">Edit Note</h3>
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:90vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="margin:0;">Edit Note</h3>
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.2rem;" onclick="document.getElementById('edit-note-modal').classList.remove('active')">×</button>
</div>
<form method="POST" enctype="multipart/form-data" id="edit-note-form">
<input type="hidden" name="update_note" value="1">
<input type="hidden" name="note_id" id="edit-note-id">
<div class="form-group">
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
<div class="form-group">
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
</div>
<div class="form-group">
<input type="text" name="label_name" id="edit-note-label" class="form-control" placeholder="Tag/Label name">
</div>
</div>
<div class="note-editor-toolbar">
<button type="button" class="note-toolbar-btn" onclick="formatNote('edit-note-content','bold')"><b>B</b></button>
@@ -307,11 +344,13 @@ $note_colors = [
$preview = strip_tags(isset($n['content']) ? (function_exists('marked') ? $n['content'] : $n['content']) : '');
$preview = mb_substr($preview, 0, 120) . (mb_strlen($preview) > 120 ? '...' : '');
?>
<div class="note-card"
<div class="note-card note-filter-item"
style="background:<?= htmlspecialchars($bg) ?>;"
data-id="<?= $n['id'] ?>"
data-title="<?= htmlspecialchars(addslashes((string)$n['title'])) ?>"
data-content="<?= htmlspecialchars(addslashes((string)$n['content'])) ?>"
data-label="<?= htmlspecialchars(strtolower(trim((string)($n['label_name'] ?? '')))) ?>"
data-label-raw="<?= htmlspecialchars(trim((string)($n['label_name'] ?? ''))) ?>"
data-color="<?= htmlspecialchars((string)$n['color']) ?>"
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
data-mine="<?= $n['is_mine'] ?>"
@@ -322,6 +361,11 @@ $note_colors = [
<?php if($n['title']): ?>
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
<?php endif; ?>
<?php if(!empty($n['label_name'])): ?>
<div style="display:inline-flex; align-items:center; gap:0.35rem; width:max-content; max-width:100%; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.7rem; font-weight:600;">
<i class="fa fa-tag" style="font-size:0.65rem;"></i> <?= htmlspecialchars($n['label_name']) ?>
</div>
<?php endif; ?>
<div class="note-card-preview"><?= htmlspecialchars($preview) ?></div>
<div class="note-card-footer">
<div style="display:flex; align-items:center; gap:0.4rem;">
@@ -403,12 +447,14 @@ function openNote(card) {
const title = card.dataset.title;
const content = card.dataset.content;
const color = card.dataset.color;
const label = card.dataset.labelRaw || '';
const image = card.dataset.image;
const isMine = card.dataset.mine === '1';
document.getElementById('edit-note-id').value = id;
document.getElementById('edit-note-title').value = title;
document.getElementById('edit-note-content').value = content;
document.getElementById('edit-note-label').value = label;
document.getElementById('edit-note-color-input').value = color;
document.getElementById('edit-note-preview').style.display = 'none';
document.getElementById('edit-note-content').style.display = 'block';
@@ -449,6 +495,19 @@ function openNote(card) {
document.getElementById('edit-note-modal').classList.add('active');
}
function filterNotes() {
const q = (document.getElementById('notes-search-input')?.value || '').toLowerCase().trim();
const lbl = (document.getElementById('notes-label-filter')?.value || '').trim();
document.querySelectorAll('.note-filter-item').forEach(card => {
const t = (card.dataset.title || '').toLowerCase();
const c = (card.dataset.content || '').toLowerCase();
const cl = (card.dataset.label || '').toLowerCase();
const okText = !q || t.includes(q) || c.includes(q) || cl.includes(q);
const okLabel = !lbl || cl === lbl;
card.style.display = (okText && okLabel) ? '' : 'none';
});
}
function deleteNote(id) {
if(!confirm('Delete this note permanently?')) return;
document.getElementById('delete-note-id').value = id;

View File

@@ -217,11 +217,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
<input type="hidden" name="delete_password" value="1">
<input type="hidden" name="password_id" id="delete-pw-id">
</form>
<div style="display:flex; justify-content:flex-end; gap:0.5rem; flex-wrap:wrap; margin-bottom:1rem;">
<button class="btn btn-secondary" onclick="showAddFolderPrompt()"><i class="fa fa-folder-plus"></i> New Folder</button>
<button class="btn" onclick="openAddPassword()"><i class="fa fa-plus"></i> Add Entry</button>
</div>
<form method="POST" id="remove-share-form" style="display:none;">
<input type="hidden" name="remove_password_share" value="1">
<input type="hidden" name="share_id" id="remove-share-id">
</form>
<!-- Add folder form (hidden) -->
<form method="POST" id="add-folder-form" style="display:none;">
@@ -234,7 +233,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
</form>
<div style="background:var(--surface); border-radius:1rem; box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border); overflow:hidden;">
<div class="vault-layout" style="min-height:calc(100vh - 200px);">
<div class="vault-layout" style="min-height:calc(100vh - 130px);">
<!-- Vault Sidebar (Folders) -->
<div class="vault-sidebar">
<div style="font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light); font-weight:700; padding:0.25rem 0.65rem; margin-bottom:0.25rem;">Folders</div>
@@ -265,6 +264,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
<!-- Vault Main -->
<div class="vault-main">
<div style="display:flex; justify-content:flex-end; gap:0.5rem; flex-wrap:wrap; margin-bottom:1rem;">
<button class="btn btn-secondary" onclick="showAddFolderPrompt()"><i class="fa fa-folder-plus"></i> New Folder</button>
<button class="btn" onclick="openAddPassword()"><i class="fa fa-plus"></i> Add Entry</button>
</div>
<!-- Search + Bulk Actions -->
<div class="vault-search">
<div style="position:relative; flex:1;">
@@ -347,11 +350,20 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
$domain = preg_replace('/^www\./', '', $domain);
}
$favicon = $domain ? "https://www.google.com/s2/favicons?domain={$domain}&sz=32" : '';
$pw_shares = [];
if($p['is_mine']) {
$pw_shares = query("SELECT ps.id, ps.share_type, ps.share_id, u.username, g.name as gname FROM password_shares ps LEFT JOIN users u ON (ps.share_type='user' AND ps.share_id=u.id) LEFT JOIN groups g ON (ps.share_type='group' AND ps.share_id=g.id) WHERE ps.password_id=?", [$p['id']])->fetchAll();
}
$shares_json = htmlspecialchars(json_encode($pw_shares), ENT_QUOTES, 'UTF-8');
?>
<div class="vault-entry pw-row"
data-pw="<?= htmlspecialchars(addslashes($p['password'])) ?>"
data-usr="<?= htmlspecialchars((string)$p['username']) ?>"
data-id="<?= $p['id'] ?>"
data-url="<?= htmlspecialchars((string)($p['url'] ?? '')) ?>"
data-notes="<?= htmlspecialchars(addslashes((string)($p['notes'] ?? ''))) ?>"
data-folder-id="<?= htmlspecialchars((string)($p['folder_id'] ?? '')) ?>"
data-shares="<?= $shares_json ?>"
onclick="openViewEntry(this)">
<!-- Checkbox (only for owned items) -->
<?php if($p['is_mine']): ?>
@@ -463,8 +475,8 @@ function openViewEntry(row) {
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
const urlEl = document.getElementById('pw-view-url');
urlEl.textContent = '';
// try to show url from data attribute if we add it - just show username for now
document.getElementById('pw-view-notes').textContent = '';
urlEl.textContent = row.dataset.url || '';
document.getElementById('pw-view-notes').textContent = row.dataset.notes || '';
document.getElementById('password-view-modal').classList.add('active');
}
@@ -492,6 +504,8 @@ function openAddPassword() {
document.getElementById('pw-edit-url').value = '';
document.getElementById('pw-edit-notes').value = '';
document.getElementById('pw-edit-folder').value = '';
document.getElementById('pw-shares-section').style.display = 'none';
document.getElementById('pw-shares-list').innerHTML = '';
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
document.getElementById('pw-edit-pw').dispatchEvent(new Event('input'));
document.getElementById('password-edit-modal').classList.add('active');
@@ -517,13 +531,48 @@ function openEditEntry(row) {
document.getElementById('pw-edit-uname').value = row.dataset.usr;
document.getElementById('pw-edit-pw').value = row.dataset.pw;
document.getElementById('pw-edit-pw').type = 'password';
document.getElementById('pw-edit-url').value = '';
document.getElementById('pw-edit-notes').value = '';
document.getElementById('pw-edit-folder').value = '';
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-delete-wrap').style.display = 'block';
renderPasswordShares(row.dataset.shares || '[]');
document.getElementById('password-edit-modal').classList.add('active');
}
function renderPasswordShares(jsonShares) {
const sec = document.getElementById('pw-shares-section');
const list = document.getElementById('pw-shares-list');
list.innerHTML = '';
let shares = [];
try { shares = JSON.parse(jsonShares || '[]'); } catch(e) { shares = []; }
if(!shares || shares.length === 0) {
sec.style.display = 'none';
return;
}
shares.forEach(sh => {
const label = sh.share_type === 'user' ? ('👤 ' + (sh.username || 'User')) : ('👥 ' + (sh.gname || 'Group'));
const chip = document.createElement('div');
chip.style.display = 'inline-flex';
chip.style.alignItems = 'center';
chip.style.gap = '0.35rem';
chip.style.fontSize = '0.72rem';
chip.style.padding = '3px 7px';
chip.style.borderRadius = '999px';
chip.style.background = 'rgba(var(--primary-rgb),0.12)';
chip.style.color = 'var(--primary)';
chip.innerHTML = `<span>${label}</span><button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;" title="Unshare" onclick="removePasswordShare(${Number(sh.id)})">×</button>`;
list.appendChild(chip);
});
sec.style.display = 'block';
}
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();
}
function deletePassword() {
if(!confirm('Delete this entry permanently?')) return;
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;

View File

@@ -138,17 +138,6 @@ function get_file_icon($name, $is_folder) {
<input type="hidden" name="item_id" id="delete-item-id">
</form>
<div class="header" style="margin-bottom:0.75rem;">
<h1 style="font-size:1.5rem; font-weight:700;">📁 Project Explorer</h1>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
<label class="btn btn-secondary" style="cursor:pointer;">
<i class="fa fa-arrow-up-from-bracket"></i> Upload File
<input type="file" id="file-upload-input" style="display:none;" onchange="handleFileSelect(this)">
</label>
<button class="btn" onclick="showNewFolderDialog()"><i class="fa fa-folder-plus"></i> New Folder</button>
</div>
</div>
<!-- Hidden folder create form -->
<form method="POST" id="new-folder-form" style="display:none;">
<input type="hidden" name="create_folder" value="1">
@@ -156,7 +145,7 @@ function get_file_icon($name, $is_folder) {
</form>
<!-- Explorer Toolbar / Breadcrumb -->
<div class="explorer-toolbar">
<div class="explorer-toolbar" style="margin-bottom:0.75rem;">
<a href="?action=projects" class="btn btn-secondary btn-sm" title="Home">
<i class="fa fa-house"></i>
</a>
@@ -173,21 +162,24 @@ function get_file_icon($name, $is_folder) {
<?php endforeach; ?>
</div>
<!-- Search -->
<input type="text" id="explorer-search" placeholder="Search..." class="form-control" style="width:180px; padding:0.4rem 0.6rem; font-size:0.85rem;" oninput="filterExplorer(this.value)">
<label class="btn btn-secondary btn-sm" style="cursor:pointer;">
<i class="fa fa-arrow-up-from-bracket"></i> Upload File
<input type="file" id="file-upload-input" style="display:none;" onchange="handleFileSelect(this)">
</label>
<button class="btn btn-sm" onclick="showNewFolderDialog()"><i class="fa fa-folder-plus"></i> New Folder</button>
</div>
<!-- Drop Zone -->
<div id="dropzone" class="dropzone" style="padding:1.5rem; margin-bottom:0.75rem;"
ondragover="this.classList.add('dragover'); return false;"
ondragleave="this.classList.remove('dragover');"
ondrop="handleDrop(event)">
<i class="fa fa-cloud-arrow-up" style="font-size:1.5rem; margin-bottom:0.5rem; display:block;"></i>
Drag & drop files here to upload
<div id="drag-upload-overlay" style="position:fixed; inset:0; z-index:1200; display:none; align-items:center; justify-content:center; pointer-events:none; background:rgba(15,23,42,0.45); backdrop-filter:blur(2px);">
<div style="background:var(--surface); border:2px dashed var(--primary); border-radius:1rem; padding:2rem 2.5rem; color:var(--text); text-align:center; box-shadow:0 12px 30px rgba(0,0,0,0.2);">
<i class="fa fa-cloud-arrow-up" style="font-size:2rem; margin-bottom:0.6rem; display:block; color:var(--primary);"></i>
<div style="font-weight:700; margin-bottom:0.2rem;">Drop files to upload</div>
<div style="font-size:0.84rem; color:var(--text-light);">Release your file anywhere on this page</div>
</div>
</div>
<!-- Detail Table -->
<div class="card" style="padding:0; overflow:hidden;">
<div class="card" style="padding:0; overflow:hidden; min-height:calc(100vh - 190px);">
<table class="explorer-table" id="explorer-table">
<thead>
<tr>
@@ -305,7 +297,7 @@ function handleFileSelect(input) {
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
hideDragOverlay();
if(e.dataTransfer.files.length > 0) uploadFile(e.dataTransfer.files[0]);
}
@@ -318,6 +310,35 @@ function uploadFile(file) {
.then(() => window.location.reload());
}
let dragCounter = 0;
const dragOverlay = document.getElementById('drag-upload-overlay');
function showDragOverlay() {
if(dragOverlay) dragOverlay.style.display = 'flex';
}
function hideDragOverlay() {
if(dragOverlay) dragOverlay.style.display = 'none';
}
document.addEventListener('dragenter', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
dragCounter++;
showDragOverlay();
});
document.addEventListener('dragover', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
e.preventDefault();
});
document.addEventListener('dragleave', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
dragCounter = Math.max(0, dragCounter - 1);
if(dragCounter === 0) hideDragOverlay();
});
document.addEventListener('drop', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
e.preventDefault();
dragCounter = 0;
handleDrop(e);
});
// ── Select all ────────────────────────────────────────────────────────────────
function explorerSelectAll(chk) {
document.querySelectorAll('.explorer-check').forEach(c => c.checked = chk.checked);