313 lines
12 KiB
HTML
313 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sam File Explorer</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
body { background-color: #121212; color: #e0e0e0; }
|
|
.file-row { cursor: pointer; transition: background 0.2s; }
|
|
.file-row:hover { background-color: #2c2c2c; }
|
|
.icon-col { width: 40px; text-align: center; }
|
|
.action-btn { opacity: 0.6; transition: opacity 0.2s; }
|
|
.file-row:hover .action-btn { opacity: 1; }
|
|
#preview-content { text-align: center; }
|
|
img.preview-media { max-width: 100%; max-height: 70vh; border-radius: 5px; }
|
|
audio.preview-media { width: 100%; margin-top: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h3><i class="fa-solid fa-folder-open text-warning me-2"></i>File Explorer</h3>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="card p-3 mb-4 bg-dark border-secondary">
|
|
<div class="row g-3">
|
|
<div class="col-md-8">
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-secondary border-secondary text-light">Path</span>
|
|
<input type="text" id="pathInput" class="form-control bg-dark text-light border-secondary" value="{{ start_path }}">
|
|
<button class="btn btn-primary" onclick="loadFiles()">Go</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-center">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="recursiveToggle">
|
|
<label class="form-check-label" for="recursiveToggle">Recursive Scan (Subfolders)</label>
|
|
</div>
|
|
<button class="btn btn-danger ms-auto d-none" id="deleteSelectedBtn" onclick="deleteSelected()">
|
|
<i class="fa-solid fa-trash-alt me-1"></i>Delete Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 30px;"><input type="checkbox" class="form-check-input" id="selectAllCheckbox"></th>
|
|
<th class="icon-col"></th>
|
|
<th>Name</th>
|
|
<th>Size</th>
|
|
<th class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fileTableBody">
|
|
<!-- Content injected by JS -->
|
|
</tbody>
|
|
</table>
|
|
<div id="loading" class="text-center py-4 d-none">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p class="mt-2 text-muted">Scanning files...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content bg-dark border-secondary">
|
|
<div class="modal-header border-secondary">
|
|
<h5 class="modal-title" id="previewTitle">Preview</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="preview-content">
|
|
<!-- Media injected here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let currentPath = "";
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initial Load
|
|
loadFiles();
|
|
|
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
|
const fileTableBody = document.getElementById('fileTableBody');
|
|
|
|
selectAllCheckbox.addEventListener('change', (e) => {
|
|
const checkboxes = fileTableBody.querySelectorAll('.file-checkbox');
|
|
checkboxes.forEach(checkbox => {
|
|
checkbox.checked = e.target.checked;
|
|
});
|
|
updateDeleteButtonVisibility();
|
|
});
|
|
|
|
fileTableBody.addEventListener('change', (e) => {
|
|
if (e.target.classList.contains('file-checkbox')) {
|
|
updateDeleteButtonVisibility();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function loadFiles() {
|
|
const path = document.getElementById('pathInput').value;
|
|
const recursive = document.getElementById('recursiveToggle').checked;
|
|
const tbody = document.getElementById('fileTableBody');
|
|
const loading = document.getElementById('loading');
|
|
|
|
tbody.innerHTML = '';
|
|
loading.classList.remove('d-none');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/scan?path=${encodeURIComponent(path)}&recursive=${recursive}`);
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
alert(data.error);
|
|
return;
|
|
}
|
|
|
|
currentPath = data.current_path;
|
|
renderTable(data.files);
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("Failed to load files.");
|
|
} finally {
|
|
loading.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
function renderTable(files) {
|
|
const tbody = document.getElementById('fileTableBody');
|
|
// tbody is cleared in loadFiles, so we just append.
|
|
|
|
// Add "Go Up" link if not at root
|
|
if (currentPath && currentPath !== '/' && currentPath.length > 1) {
|
|
let parentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
|
|
if (parentPath === '') parentPath = '/';
|
|
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td class="icon-col"><i class="fa-solid fa-arrow-turn-up text-secondary"></i></td>
|
|
<td>..</td>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="text-end"></td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
if (files.length === 0) {
|
|
// If we haven't added the "up" directory, show no files.
|
|
if (tbody.childElementCount === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No files found.</td></tr>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
files.forEach(file => {
|
|
const tr = document.createElement('tr');
|
|
|
|
// Icon logic
|
|
let icon = 'fa-file';
|
|
let color = 'text-secondary';
|
|
if (file.is_dir) { icon = 'fa-folder'; color = 'text-warning'; }
|
|
else if (file.mime.startsWith('image')) { icon = 'fa-image'; color = 'text-info'; }
|
|
else if (file.mime.startsWith('audio')) { icon = 'fa-music'; color = 'text-success'; }
|
|
|
|
// Size formatting
|
|
const size = file.is_dir ? '-' : (file.size / 1024).toFixed(1) + ' KB';
|
|
|
|
tr.innerHTML = `
|
|
<td><input type="checkbox" class="form-check-input file-checkbox" data-path="${file.path.replace(/\\/g, '\\\\')}"></td>
|
|
<td class="icon-col"><i class="fa-solid ${icon} ${color}"></i></td>
|
|
<td class="file-name-cell">${file.name}</td>
|
|
<td>${size}</td>
|
|
<td class="text-end">
|
|
${getActions(file)}
|
|
</td>
|
|
`;
|
|
|
|
// Row click should not trigger when clicking on interactive elements
|
|
tr.addEventListener('click', (e) => {
|
|
if (e.target.matches('input, button, a, i')) return; // Ignore clicks on actions/checkboxes
|
|
if (file.is_dir) {
|
|
enterDir(file.path);
|
|
}
|
|
});
|
|
|
|
// Make the text part of the cell clickable for directory navigation
|
|
const fileNameCell = tr.querySelector('.file-name-cell');
|
|
if(file.is_dir) {
|
|
fileNameCell.classList.add('file-row');
|
|
fileNameCell.onclick = () => enterDir(file.path);
|
|
}
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function getActions(file) {
|
|
if (file.is_dir) return '';
|
|
|
|
let previewBtn = '';
|
|
if (file.mime.startsWith('image') || file.mime.startsWith('audio')) {
|
|
previewBtn = `<button class="btn btn-sm btn-outline-light action-btn me-1" onclick="previewFile('${file.path.replace(/\\/g, '\\\\')}', '${file.mime}', '${file.name}')" title="Preview"><i class="fa-solid fa-eye"></i></button>`;
|
|
}
|
|
|
|
return `
|
|
${previewBtn}
|
|
<a href="${API_BASE}/download?path=${encodeURIComponent(file.path)}" class="btn btn-sm btn-outline-primary action-btn me-1" title="Download"><i class="fa-solid fa-download"></i></a>
|
|
<button class="btn btn-sm btn-outline-danger action-btn" onclick="deleteFile('${file.path.replace(/\\/g, '\\\\')}')" title="Delete"><i class="fa-solid fa-trash"></i></button>
|
|
`;
|
|
}
|
|
|
|
function enterDir(path) {
|
|
document.getElementById('pathInput').value = path;
|
|
document.getElementById('recursiveToggle').checked = false; // Reset recursive when diving into folders
|
|
loadFiles();
|
|
}
|
|
|
|
function previewFile(path, mime, name) {
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
const container = document.getElementById('preview-content');
|
|
document.getElementById('previewTitle').innerText = name;
|
|
|
|
const url = `${API_BASE}/preview?path=${encodeURIComponent(path)}`;
|
|
|
|
if (mime.startsWith('image')) {
|
|
container.innerHTML = `<img src="${url}" class="preview-media" alt="Preview">`;
|
|
} else if (mime.startsWith('audio')) {
|
|
container.innerHTML = `<audio controls class="preview-media" autoplay><source src="${url}" type="${mime}"></audio>`;
|
|
}
|
|
|
|
modal.show();
|
|
|
|
// Stop audio when modal closes
|
|
document.getElementById('previewModal').addEventListener('hidden.bs.modal', () => {
|
|
container.innerHTML = '';
|
|
});
|
|
}
|
|
|
|
async function deleteFile(path) {
|
|
if (!confirm("Are you sure you want to delete this file? This cannot be undone.")) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/delete?path=${encodeURIComponent(path)}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
loadFiles(); // Refresh list
|
|
} else {
|
|
alert("Error deleting file: " + (data.error || "Unknown error"));
|
|
}
|
|
} catch (err) {
|
|
alert("Network error");
|
|
}
|
|
}
|
|
|
|
async function deleteSelected() {
|
|
const selectedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
|
|
if (selectedCheckboxes.length === 0) {
|
|
alert("Please select files to delete.");
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to delete ${selectedCheckboxes.length} selected files? This cannot be undone.`)) return;
|
|
|
|
const paths = Array.from(selectedCheckboxes).map(cb => cb.dataset.path);
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/delete_multiple`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ paths: paths })
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
loadFiles(); // Refresh list
|
|
} else {
|
|
alert("Error deleting files: " + (data.error || "Unknown error"));
|
|
}
|
|
} catch (err) {
|
|
alert("Network error during multiple delete.");
|
|
}
|
|
}
|
|
|
|
function updateDeleteButtonVisibility() {
|
|
const selectedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
|
|
const deleteBtn = document.getElementById('deleteSelectedBtn');
|
|
if (selectedCheckboxes.length > 0) {
|
|
deleteBtn.classList.remove('d-none');
|
|
} else {
|
|
deleteBtn.classList.add('d-none');
|
|
}
|
|
}
|
|
</script>
|
|
|