major change

This commit is contained in:
root
2026-03-27 00:03:48 +07:00
parent 9b1d5753cb
commit f89fcbee1c
9 changed files with 2143 additions and 463 deletions

View File

@@ -1,4 +1,3 @@
<?php
$parent_id = $_GET['folder'] ?? null;
@@ -20,29 +19,29 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_item'])) {
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_file']) && isset($_FILES['file'])) {
$name = $_FILES['file']['name'];
$tmp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$path = 'uploads/' . time() . '_' . $name;
move_uploaded_file($tmp_name, $path);
$stmt = $pdo->prepare("INSERT INTO projects (name, is_folder, parent_id, file_path, owner_id) VALUES (?, 0, ?, ?, ?)");
$stmt->execute([$name, $parent_id, $path, $user_id]);
echo "OK"; exit; // For AJAX
echo "OK"; exit;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_project'])) {
$pr_id = $_POST['project_id'];
$share = $_POST['share_target']; // 'user_1'
$parts = explode('_', $share);
$stmt = $pdo->prepare("INSERT INTO project_shares (project_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$pr_id, $parts[0], $parts[1]]);
$pr = query("SELECT name FROM projects WHERE id=?", [$pr_id])->fetch();
$msg = "A project/file '{$pr['name']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
$share = $_POST['share_target'];
$parts = explode('_', $share, 2);
if(count($parts)===2) {
$stmt = $pdo->prepare("INSERT INTO project_shares (project_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$pr_id, $parts[0], $parts[1]]);
$pr = query("SELECT name FROM projects WHERE id=?", [$pr_id])->fetch();
$msg = "A project/file '{$pr['name']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
}
}
// Fetch items in current folder
// ── Fetch current folder content ──────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0';
@@ -67,80 +66,210 @@ $items = $stmt->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
// ── Build breadcrumb path ─────────────────────────────────────────────────────
$breadcrumbs = [];
$cur_id = $parent_id;
while($cur_id) {
$node = query("SELECT id, name, parent_id FROM projects WHERE id=?", [$cur_id])->fetch();
if(!$node) break;
array_unshift($breadcrumbs, $node);
$cur_id = $node['parent_id'];
}
// ── Helper: file size ─────────────────────────────────────────────────────────
function fmt_size($bytes) {
if(!$bytes) return '—';
if($bytes < 1024) return $bytes . ' B';
if($bytes < 1048576) return round($bytes/1024,1) . ' KB';
return round($bytes/1048576,1) . ' MB';
}
function get_file_icon($name, $is_folder) {
if($is_folder) return '📁';
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$map = [
'pdf'=>'📄','doc'=>'📝','docx'=>'📝','xls'=>'📊','xlsx'=>'📊','ppt'=>'📑','pptx'=>'📑',
'jpg'=>'🖼','jpeg'=>'🖼','png'=>'🖼','gif'=>'🖼','webp'=>'🖼','svg'=>'🖼',
'mp4'=>'🎬','avi'=>'🎬','mov'=>'🎬','mp3'=>'🎵','wav'=>'🎵',
'zip'=>'🗜','rar'=>'🗜','7z'=>'🗜','tar'=>'🗜',
'php'=>'⚙','js'=>'⚙','css'=>'🎨','html'=>'🌐','txt'=>'📃','md'=>'📃',
'sh'=>'💻','py'=>'🐍',
];
return $map[$ext] ?? '📄';
}
?>
<div class="header">
<h1>Project Files Explorer</h1>
</div>
<div class="flex-between">
<?php if($parent_id):
$parent = query("SELECT parent_id FROM projects WHERE id=?", [$parent_id])->fetch();
?>
<a href="?action=projects&folder=<?= $parent['parent_id'] ?>" class="btn btn-secondary">← Back</a>
<?php else: ?>
<div>Root Directory</div>
<?php endif; ?>
<div style="display:flex; gap:1rem;">
<form method="POST" style="display:flex; gap:0.5rem;">
<input type="text" name="name" class="form-control" placeholder="New Folder Name" required>
<button type="submit" name="create_folder" class="btn">New Folder</button>
</form>
</div>
</div>
<div class="card" style="margin-top:1.5rem;">
<!-- Drag Drop Zone -->
<div id="dropzone" class="dropzone" ondragover="this.classList.add('dragover'); return false;" ondragleave="this.classList.remove('dragover');" ondrop="handleDrop(event)">
SVG Drop files here to upload instantly
<br><br><span style="color:var(--text-light); font-size:0.875rem;">(HTML5 Drag & Drop)</span>
</div>
<div style="margin-top:2rem;">
<?php foreach($items as $item): ?>
<div class="project-item">
<div style="display:flex; align-items:center; gap:0.5rem; flex:1;">
<span style="font-size:1.5rem;"><?= $item['is_folder'] ? '📁' : '📄' ?></span>
<?php if($item['is_folder']): ?>
<a href="?action=projects&folder=<?= $item['id'] ?>" style="font-weight:600;"><?= htmlspecialchars($item['name']) ?></a>
<?php else: ?>
<a href="<?= htmlspecialchars($item['file_path']) ?>" target="_blank"><?= htmlspecialchars($item['name']) ?></a>
<?php endif; ?>
</div>
<div style="width:200px; color:var(--text-light); font-size:0.875rem;">
Owner: <?= htmlspecialchars($item['owner_name']) ?>
<?= !$item['is_mine'] ? '<span class="badge" style="background:#10B981; font-size:10px;">Shared</span>' : '' ?>
</div>
<div style="display:flex; gap:0.5rem;">
<?php if($item['is_mine']): ?>
<form method="POST" style="display:inline; display:flex; gap:0.5rem;">
<input type="hidden" name="project_id" value="<?= $item['id'] ?>">
<select name="share_target" class="form-control" style="width:auto; padding:0.25rem;">
<?php foreach($all_users as $u): ?><option value="user_<?= $u['id'] ?>">User: <?= htmlspecialchars($u['username']) ?></option><?php endforeach; ?>
<?php foreach($all_groups as $g): ?><option value="group_<?= $g['id'] ?>">Group: <?= htmlspecialchars($g['name']) ?></option><?php endforeach; ?>
</select>
<button type="submit" name="share_project" class="btn btn-sm" title="Share">🔗</button>
</form>
<form method="POST" style="display:inline; display:flex; gap:0.5rem;" onsubmit="return confirm('Delete this item?')">
<input type="hidden" name="item_id" value="<?= $item['id'] ?>">
<button type="button" class="btn btn-sm btn-secondary" title="Rename" onclick="renameItem(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['name'])) ?>')">✏️</button>
<button type="submit" name="delete_item" class="btn btn-sm btn-danger" title="Delete">🗑️</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php if(!$items): ?><p style="margin-top:1rem;">Empty folder.</p><?php endif; ?>
</div>
</div>
<!-- Rename form hidden -->
<!-- Rename form -->
<form method="POST" id="rename-form" style="display:none;">
<input type="hidden" name="edit_item" value="1">
<input type="hidden" name="item_id" id="rn-item_id">
<input type="hidden" name="new_name" id="rn-new_name">
</form>
<!-- Share Modal -->
<div class="modal-overlay" id="share-modal">
<div class="modal-content" style="max-width:400px;">
<h3 style="margin-bottom:1rem; font-size:1.05rem;"><i class="fa fa-share-nodes"></i> Share Item</h3>
<form method="POST" id="share-form">
<input type="hidden" name="share_project" value="1">
<input type="hidden" name="project_id" id="share-project-id">
<div class="form-group">
<label>Share with</label>
<select name="share_target" class="form-control">
<?php foreach($all_users as $u): ?>
<option value="user_<?= $u['id'] ?>">👤 <?= htmlspecialchars($u['username']) ?></option>
<?php endforeach; ?>
<?php foreach($all_groups as $g): ?>
<option value="group_<?= $g['id'] ?>">👥 <?= htmlspecialchars($g['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex; gap:0.5rem; justify-content:flex-end; margin-top:1rem;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('share-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-share-nodes"></i> Share</button>
</div>
</form>
</div>
</div>
<!-- Delete form -->
<form method="POST" id="delete-item-form" style="display:none;">
<input type="hidden" name="delete_item" value="1">
<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">
<input type="hidden" name="name" id="new-folder-name">
</form>
<!-- Explorer Toolbar / Breadcrumb -->
<div class="explorer-toolbar">
<a href="?action=projects" class="btn btn-secondary btn-sm" title="Home">
<i class="fa fa-house"></i>
</a>
<a href="javascript:history.back()" class="btn btn-secondary btn-sm" title="Back">
<i class="fa fa-arrow-left"></i>
</a>
<div class="explorer-path">
<a class="explorer-path-part <?= !$parent_id?'current':'' ?>" href="?action=projects">Root</a>
<?php foreach($breadcrumbs as $bc): ?>
<span class="explorer-path-sep">›</span>
<a class="explorer-path-part <?= $bc['id']==$parent_id?'current':'' ?>" href="?action=projects&folder=<?= $bc['id'] ?>">
<?= htmlspecialchars($bc['name']) ?>
</a>
<?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)">
</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>
<!-- Detail Table -->
<div class="card" style="padding:0; overflow:hidden;">
<table class="explorer-table" id="explorer-table">
<thead>
<tr>
<th style="width:30px;"><input type="checkbox" id="explorer-select-all" onclick="explorerSelectAll(this)"></th>
<th onclick="sortExplorer('name')" style="min-width:200px;">
<i class="fa fa-sort" style="font-size:0.7rem; margin-right:4px;"></i>Name
</th>
<th onclick="sortExplorer('type')" style="width:80px;">Type</th>
<th onclick="sortExplorer('owner')" style="width:120px;">Owner</th>
<th onclick="sortExplorer('size')" style="width:80px;">Size</th>
<th style="width:140px;">Shared</th>
<th style="width:160px; text-align:right;">Actions</th>
</tr>
</thead>
<tbody id="explorer-tbody">
<?php if(empty($items)): ?>
<tr><td colspan="7" style="text-align:center; padding:3rem; color:var(--text-light);">
<i class="fa fa-folder-open" style="font-size:2rem; display:block; margin-bottom:0.5rem;"></i>
This folder is empty.
</td></tr>
<?php endif; ?>
<?php foreach($items as $item):
$ext = $item['is_folder'] ? 'folder' : strtoupper(pathinfo($item['name'], PATHINFO_EXTENSION) ?: 'File');
$size = (!$item['is_folder'] && $item['file_path'] && file_exists($item['file_path'])) ? filesize($item['file_path']) : null;
$icon = get_file_icon($item['name'], $item['is_folder']);
?>
<tr class="explorer-row" data-name="<?= htmlspecialchars($item['name']) ?>" data-type="<?= $item['is_folder']?'folder':'file' ?>">
<td><input type="checkbox" class="explorer-check" value="<?= $item['id'] ?>"></td>
<td>
<div class="explorer-name">
<span class="explorer-icon"><?= $icon ?></span>
<?php if($item['is_folder']): ?>
<a href="?action=projects&folder=<?= $item['id'] ?>"><?= htmlspecialchars($item['name']) ?></a>
<?php else: ?>
<a href="<?= htmlspecialchars($item['file_path']) ?>" target="_blank"><?= htmlspecialchars($item['name']) ?></a>
<?php endif; ?>
</div>
</td>
<td style="color:var(--text-light); font-size:0.8rem;"><?= htmlspecialchars($ext) ?></td>
<td style="font-size:0.8rem;">
<?= htmlspecialchars($item['owner_name']) ?>
<?php if(!$item['is_mine']): ?><span class="badge" style="font-size:0.6rem; padding:1px 5px; margin-left:2px;">Shared</span><?php endif; ?>
</td>
<td style="color:var(--text-light); font-size:0.8rem;"><?= $size !== null ? fmt_size($size) : '—' ?></td>
<td style="font-size:0.8rem; color:var(--text-light);">
<?php
// Show shares
$shares = query("SELECT ps.share_type, ps.share_id, u.username, g.name as gname FROM project_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.project_id=?", [$item['id']])->fetchAll();
foreach($shares as $sh) {
echo '<span style="font-size:0.72rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 5px; border-radius:8px; margin:1px; display:inline-block;">';
echo $sh['share_type']==='user' ? ('👤 '.htmlspecialchars($sh['username']??'')) : ('👥 '.htmlspecialchars($sh['gname']??''));
echo '</span>';
}
if(!$shares) echo '<span style="color:var(--text-light); font-size:0.8rem;">—</span>';
?>
</td>
<td>
<div class="explorer-actions" style="justify-content:flex-end;">
<?php if($item['is_mine']): ?>
<button class="btn btn-sm btn-secondary" onclick="openShare(<?= $item['id'] ?>)" title="Share"><i class="fa fa-share-nodes"></i></button>
<button class="btn btn-sm btn-secondary" onclick="renameItem(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Rename"><i class="fa fa-pen"></i></button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fa fa-trash"></i></button>
<?php else: ?>
<span style="font-size:0.75rem; color:var(--text-light); padding:0 0.5rem;">View only</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Status Bar -->
<div class="explorer-status-bar">
<span id="explorer-count"><?= count($items) ?> item<?= count($items)!=1?'s':'' ?></span>
<span id="explorer-selected-count" style="display:none; color:var(--primary); font-weight:600;"></span>
<span style="margin-left:auto;">
<?= $parent_id ? 'Folder: '.htmlspecialchars(end($breadcrumbs)['name'] ?? '') : 'Root directory' ?>
</span>
</div>
<script>
function renameItem(id, currName) {
let nn = prompt("Enter new name:", currName);
@@ -151,24 +280,91 @@ function renameItem(id, currName) {
}
}
function deleteItem(id, name) {
if(!confirm(`Delete "${name}"? This cannot be undone.`)) return;
document.getElementById('delete-item-id').value = id;
document.getElementById('delete-item-form').submit();
}
function openShare(id) {
document.getElementById('share-project-id').value = id;
document.getElementById('share-modal').classList.add('active');
}
function showNewFolderDialog() {
const name = prompt('New folder name:');
if(name && name.trim()) {
document.getElementById('new-folder-name').value = name.trim();
document.getElementById('new-folder-form').submit();
}
}
function handleFileSelect(input) {
if(input.files.length > 0) uploadFile(input.files[0]);
}
function handleDrop(e) {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
let dt = e.dataTransfer;
let files = dt.files;
if(files.length > 0) {
let formData = new FormData();
formData.append('upload_file', '1');
formData.append('file', files[0]);
fetch('?action=projects&folder=<?= $parent_id ?>', {
method: 'POST',
body: formData
}).then(res => {
window.location.reload();
});
if(e.dataTransfer.files.length > 0) uploadFile(e.dataTransfer.files[0]);
}
function uploadFile(file) {
showToast('Uploading ' + file.name + '...');
let formData = new FormData();
formData.append('upload_file', '1');
formData.append('file', file);
fetch('?action=projects&folder=<?= $parent_id ?>', { method: 'POST', body: formData })
.then(() => window.location.reload());
}
// ── Select all ────────────────────────────────────────────────────────────────
function explorerSelectAll(chk) {
document.querySelectorAll('.explorer-check').forEach(c => c.checked = chk.checked);
updateSelectedCount();
}
document.querySelectorAll('.explorer-check').forEach(c => c.addEventListener('change', updateSelectedCount));
function updateSelectedCount() {
const n = document.querySelectorAll('.explorer-check:checked').length;
const el = document.getElementById('explorer-selected-count');
if(n > 0) {
el.textContent = n + ' selected';
el.style.display = 'inline';
} else {
el.style.display = 'none';
}
}
// ── Search filter ─────────────────────────────────────────────────────────────
function filterExplorer(q) {
q = q.toLowerCase();
let visible = 0;
document.querySelectorAll('#explorer-tbody tr.explorer-row').forEach(row => {
const name = row.dataset.name || '';
if(name.toLowerCase().includes(q)) { row.style.display = ''; visible++; }
else row.style.display = 'none';
});
document.getElementById('explorer-count').textContent = visible + ' item' + (visible!==1?'s':'');
}
// ── Sort ──────────────────────────────────────────────────────────────────────
let sortDir = {};
function sortExplorer(col) {
sortDir[col] = !sortDir[col];
const tbody = document.getElementById('explorer-tbody');
const rows = Array.from(tbody.querySelectorAll('tr.explorer-row'));
rows.sort((a, b) => {
let av = '', bv = '';
if(col==='name') { av=a.dataset.name||''; bv=b.dataset.name||''; }
if(col==='type') { av=a.dataset.type||''; bv=b.dataset.type||''; }
// folders first for type sort
if(col==='type' && av===bv) return 0;
const cmp = av.localeCompare(bv);
return sortDir[col] ? cmp : -cmp;
});
// Folders always first
const folders = rows.filter(r => r.dataset.type==='folder');
const files = rows.filter(r => r.dataset.type!=='folder');
[...folders,...files].forEach(r => tbody.appendChild(r));
}
</script>