420 lines
20 KiB
PHP
420 lines
20 KiB
PHP
<?php
|
||
$parent_id = $_GET['folder'] ?? null;
|
||
|
||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_folder'])) {
|
||
$stmt = $pdo->prepare("INSERT INTO projects (name, is_folder, parent_id, owner_id) VALUES (?, 1, ?, ?)");
|
||
$stmt->execute([$_POST['name'], $parent_id, $user_id]);
|
||
}
|
||
|
||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_item'])) {
|
||
$stmt = $pdo->prepare("DELETE FROM projects WHERE id=? AND owner_id=?");
|
||
$stmt->execute([$_POST['item_id'], $user_id]);
|
||
}
|
||
|
||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_item'])) {
|
||
$stmt = $pdo->prepare("UPDATE projects SET name=? WHERE id=? AND owner_id=?");
|
||
$stmt->execute([$_POST['new_name'], $_POST['item_id'], $user_id]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_project'])) {
|
||
$pr_id = $_POST['project_id'];
|
||
$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);
|
||
}
|
||
}
|
||
|
||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_project_share'])) {
|
||
$share_id = (int)$_POST['share_id'];
|
||
$chk = query("SELECT ps.id FROM project_shares ps JOIN projects p ON ps.project_id=p.id WHERE ps.id=? AND p.owner_id=?", [$share_id, $user_id])->fetch();
|
||
if($chk) {
|
||
$pdo->prepare("DELETE FROM project_shares WHERE id=?")->execute([$share_id]);
|
||
}
|
||
}
|
||
|
||
// ── 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';
|
||
|
||
$parent_cond = $parent_id ? "p.parent_id = ?" : "p.parent_id IS NULL";
|
||
$sql = "SELECT p.*, (p.owner_id = ?) as is_mine, u.username as owner_name FROM projects p
|
||
LEFT JOIN project_shares ps ON p.id = ps.project_id
|
||
LEFT JOIN users u ON p.owner_id = u.id
|
||
WHERE ($parent_cond) AND (
|
||
p.owner_id = ?
|
||
OR (ps.share_type='user' AND ps.share_id = ?)
|
||
OR (ps.share_type='group' AND ps.share_id IN ($group_in))
|
||
) GROUP BY p.id ORDER BY p.is_folder DESC, p.name ASC";
|
||
|
||
$params = [$user_id];
|
||
if($parent_id) $params[] = $parent_id;
|
||
$params = array_merge($params, [$user_id, $user_id], $my_group_ids);
|
||
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
$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] ?? '📄';
|
||
}
|
||
?>
|
||
|
||
<!-- 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>
|
||
|
||
<form method="POST" id="remove-share-form" style="display:none;">
|
||
<input type="hidden" name="remove_project_share" value="1">
|
||
<input type="hidden" name="share_id" id="remove-share-id">
|
||
</form>
|
||
|
||
<!-- 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" style="margin-bottom:0.75rem;">
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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; min-height:calc(100vh - 190px);">
|
||
<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.id, 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-flex; align-items:center; gap:3px;">';
|
||
echo $sh['share_type']==='user' ? ('👤 '.htmlspecialchars($sh['username']??'')) : ('👥 '.htmlspecialchars($sh['gname']??''));
|
||
if($item['is_mine']) {
|
||
echo '<button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;" title="Unshare" onclick="removeProjectShare('.$sh['id'].')">×</button>';
|
||
}
|
||
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);
|
||
if(nn && nn.trim()!=='' && nn !== currName) {
|
||
document.getElementById('rn-item_id').value = id;
|
||
document.getElementById('rn-new_name').value = nn;
|
||
document.getElementById('rename-form').submit();
|
||
}
|
||
}
|
||
|
||
function deleteItem(id, name) {
|
||
appConfirm(`Delete "${name}"? This cannot be undone.`).then(y => {
|
||
if(y) {
|
||
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 removeProjectShare(shareId) {
|
||
appConfirm('Unshare this item?').then(y => {
|
||
if(y) {
|
||
document.getElementById('remove-share-id').value = shareId;
|
||
document.getElementById('remove-share-form').submit();
|
||
}
|
||
});
|
||
}
|
||
|
||
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();
|
||
hideDragOverlay();
|
||
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());
|
||
}
|
||
|
||
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);
|
||
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>
|