710 lines
22 KiB
HTML
710 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Drive - General Bots</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: var(--bg-primary, #0f172a);
|
|
color: var(--text-primary, #f1f5f9);
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.drive-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.drive-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px;
|
|
background: var(--bg-secondary, #1e293b);
|
|
border-radius: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.breadcrumb {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
color: var(--text-secondary, #94a3b8);
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
cursor: pointer;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.breadcrumb-item:hover {
|
|
color: var(--accent-color, #3b82f6);
|
|
}
|
|
|
|
.breadcrumb-separator {
|
|
color: var(--text-tertiary, #475569);
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--accent-color, #3b82f6);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--accent-hover, #2563eb);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--bg-tertiary, #334155);
|
|
color: var(--text-primary, #f1f5f9);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: var(--bg-quaternary, #475569);
|
|
}
|
|
|
|
.drive-content {
|
|
flex: 1;
|
|
background: var(--bg-secondary, #1e293b);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.file-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.file-item {
|
|
background: var(--bg-tertiary, #334155);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: var(--bg-quaternary, #475569);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.file-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 14px;
|
|
word-break: break-word;
|
|
color: var(--text-primary, #f1f5f9);
|
|
}
|
|
|
|
.file-info {
|
|
font-size: 12px;
|
|
color: var(--text-secondary, #94a3b8);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.file-actions {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
display: none;
|
|
background: var(--bg-primary, #0f172a);
|
|
border-radius: 4px;
|
|
padding: 4px;
|
|
}
|
|
|
|
.file-item:hover .file-actions {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.action-btn {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary, #94a3b8);
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: var(--accent-color, #3b82f6);
|
|
color: white;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 200px;
|
|
color: var(--text-secondary, #94a3b8);
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid var(--bg-tertiary, #334155);
|
|
border-top: 3px solid var(--accent-color, #3b82f6);
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: var(--text-secondary, #94a3b8);
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 64px;
|
|
margin-bottom: 16px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--bg-secondary, #1e293b);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
}
|
|
|
|
.modal-header {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
color: var(--text-secondary, #94a3b8);
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid var(--bg-tertiary, #334155);
|
|
border-radius: 8px;
|
|
background: var(--bg-primary, #0f172a);
|
|
color: var(--text-primary, #f1f5f9);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent-color, #3b82f6);
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.upload-area {
|
|
border: 2px dashed var(--bg-tertiary, #334155);
|
|
border-radius: 8px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
border-color: var(--accent-color, #3b82f6);
|
|
background: var(--bg-tertiary, #334155);
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: var(--accent-color, #3b82f6);
|
|
background: var(--bg-tertiary, #334155);
|
|
}
|
|
|
|
.notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: var(--bg-secondary, #1e293b);
|
|
padding: 16px 24px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
display: none;
|
|
z-index: 2000;
|
|
}
|
|
|
|
.notification.show {
|
|
display: block;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.notification.success {
|
|
border-left: 4px solid #10b981;
|
|
}
|
|
|
|
.notification.error {
|
|
border-left: 4px solid #ef4444;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="drive-container">
|
|
<div class="drive-header">
|
|
<div class="breadcrumb">
|
|
<span class="breadcrumb-item" data-path="/">📁 Drive</span>
|
|
<span class="breadcrumb-separator">/</span>
|
|
<span class="breadcrumb-item current"></span>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn btn-secondary" onclick="createFolder()">
|
|
📁 New Folder
|
|
</button>
|
|
<button class="btn btn-primary" onclick="uploadFile()">
|
|
⬆️ Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="drive-content">
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
Loading files...
|
|
</div>
|
|
<div class="file-grid" id="fileGrid" style="display: none;"></div>
|
|
<div class="empty-state" id="emptyState" style="display: none;">
|
|
<div class="empty-icon">📂</div>
|
|
<p>This folder is empty</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div class="modal" id="uploadModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">Upload File</div>
|
|
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
|
|
<p>📤 Click to select or drag files here</p>
|
|
<input type="file" id="fileInput" style</p>="display: none;" multiple>
|
|
</div>
|
|
<div id="uploadProgress"></div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" onclick="closeUploadModal()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Folder Modal -->
|
|
<div class="modal" id="folderModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">Create New Folder</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Folder Name</label>
|
|
<input type="text" class="form-input" id="folderName" placeholder="Enter folder name">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" onclick="closeFolderModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="submitCreateFolder()">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification -->
|
|
<div class="notification" id="notification"></div>
|
|
|
|
<script>
|
|
const API_BASE = window.location.origin;
|
|
let currentPath = '/';
|
|
|
|
// Load files on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadFiles(currentPath);
|
|
setupDragAndDrop();
|
|
});
|
|
|
|
// Load files from API
|
|
async function loadFiles(path) {
|
|
const loading = document.getElementById('loading');
|
|
const fileGrid = document.getElementById('fileGrid');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
loading.style.display = 'flex';
|
|
fileGrid.style.display = 'none';
|
|
emptyState.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/drive/list?path=${encodeURIComponent(path)}`);
|
|
const data = await response.json();
|
|
const files = Array.isArray(data) ? data : [];
|
|
|
|
loading.style.display = 'none';
|
|
|
|
if (files.length === 0) {
|
|
emptyState.style.display = 'flex';
|
|
} else {
|
|
fileGrid.style.display = 'grid';
|
|
renderFiles(files);
|
|
}
|
|
|
|
updateBreadcrumb(path);
|
|
} catch (error) {
|
|
console.error('Failed to load files:', error);
|
|
showNotification('Failed to load files', 'error');
|
|
loading.style.display = 'none';
|
|
emptyState.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
// Render files in grid
|
|
function renderFiles(files) {
|
|
const fileGrid = document.getElementById('fileGrid');
|
|
fileGrid.innerHTML = '';
|
|
|
|
files.forEach(file => {
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
fileItem.onclick = () => handleFileClick(file);
|
|
|
|
const icon = file.is_dir ? '📁' : getFileIcon(file.name);
|
|
const size = file.is_dir ? '' : formatBytes(file.size);
|
|
|
|
fileItem.innerHTML = `
|
|
<div class="file-icon">${icon}</div>
|
|
<div class="file-name">${file.name}</div>
|
|
<div class="file-info">${size}</div>
|
|
<div class="file-actions">
|
|
<button class="action-btn" onclick="event.stopPropagation(); downloadFile('${file.path}')">⬇️</button>
|
|
<button class="action-btn" onclick="event.stopPropagation(); deleteFile('${file.path}')">🗑️</button>
|
|
</div>
|
|
`;
|
|
|
|
fileGrid.appendChild(fileItem);
|
|
});
|
|
}
|
|
|
|
// Handle file/folder click
|
|
function handleFileClick(file) {
|
|
if (file.is_dir) {
|
|
currentPath = file.path;
|
|
loadFiles(currentPath);
|
|
} else {
|
|
downloadFile(file.path);
|
|
}
|
|
}
|
|
|
|
// Get file icon based on extension
|
|
function getFileIcon(filename) {
|
|
const ext = filename.split('.').pop().toLowerCase();
|
|
const icons = {
|
|
'pdf': '📄',
|
|
'doc': '📝', 'docx': '📝',
|
|
'xls': '📊', 'xlsx': '📊',
|
|
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
|
|
'mp4': '🎬', 'avi': '🎬', 'mov': '🎬',
|
|
'mp3': '🎵', 'wav': '🎵',
|
|
'zip': '📦', 'rar': '📦', 'tar': '📦',
|
|
'txt': '📃',
|
|
'js': '💻', 'html': '💻', 'css': '💻', 'py': '💻',
|
|
};
|
|
return icons[ext] || '📄';
|
|
}
|
|
|
|
// Format bytes to human readable
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
// Update breadcrumb
|
|
function updateBreadcrumb(path) {
|
|
const breadcrumb = document.querySelector('.breadcrumb');
|
|
const parts = path.split('/').filter(p => p);
|
|
|
|
breadcrumb.innerHTML = '<span class="breadcrumb-item" onclick="navigateTo(\'/\')">📁 Drive</span>';
|
|
|
|
let accumulated = '';
|
|
parts.forEach((part, index) => {
|
|
accumulated += '/' + part;
|
|
const isLast = index === parts.length - 1;
|
|
breadcrumb.innerHTML += ` <span class="breadcrumb-separator">/</span> `;
|
|
if (isLast) {
|
|
breadcrumb.innerHTML += `<span class="breadcrumb-item current">${part}</span>`;
|
|
} else {
|
|
const pathCopy = accumulated;
|
|
breadcrumb.innerHTML += `<span class="breadcrumb-item" onclick="navigateTo('${pathCopy}')">${part}</span>`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Navigate to path
|
|
function navigateTo(path) {
|
|
currentPath = path;
|
|
loadFiles(path);
|
|
}
|
|
|
|
// Upload file modal
|
|
function uploadFile() {
|
|
document.getElementById('uploadModal').classList.add('active');
|
|
}
|
|
|
|
function closeUploadModal() {
|
|
document.getElementById('uploadModal').classList.remove('active');
|
|
document.getElementById('fileInput').value = '';
|
|
}
|
|
|
|
// Handle file selection
|
|
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
await uploadFiles(files);
|
|
}
|
|
});
|
|
|
|
// Upload files to API
|
|
async function uploadFiles(files) {
|
|
const progress = document.getElementById('uploadProgress');
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
progress.innerHTML = `Uploading ${file.name}... (${i + 1}/${files.length})`;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('path', currentPath);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/drive/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
showNotification(`Uploaded ${file.name}`, 'success');
|
|
} else {
|
|
showNotification(`Failed to upload ${file.name}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
showNotification(`Error uploading ${file.name}`, 'error');
|
|
}
|
|
}
|
|
|
|
closeUploadModal();
|
|
loadFiles(currentPath);
|
|
}
|
|
|
|
// Drag and drop
|
|
function setupDragAndDrop() {
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, () => {
|
|
uploadArea.classList.add('dragover');
|
|
}, false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
uploadArea.addEventListener(eventName, () => {
|
|
uploadArea.classList.remove('dragover');
|
|
}, false);
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
uploadFiles(files);
|
|
}
|
|
}, false);
|
|
}
|
|
|
|
// Create folder modal
|
|
function createFolder() {
|
|
document.getElementById('folderModal').classList.add('active');
|
|
document.getElementById('folderName').value = '';
|
|
}
|
|
|
|
function closeFolderModal() {
|
|
document.getElementById('folderModal').classList.remove('active');
|
|
}
|
|
|
|
async function submitCreateFolder() {
|
|
const folderName = document.getElementById('folderName').value.trim();
|
|
|
|
if (!folderName) {
|
|
showNotification('Please enter a folder name', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/drive/folder`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
path: currentPath,
|
|
name: folderName
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
showNotification('Folder created successfully', 'success');
|
|
closeFolderModal();
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('Failed to create folder', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Create folder error:', error);
|
|
showNotification('Error creating folder', 'error');
|
|
}
|
|
}
|
|
|
|
// Download file
|
|
async function downloadFile(path) {
|
|
window.open(`${API_BASE}/api/drive/download${path}`, '_blank');
|
|
}
|
|
|
|
// Delete file
|
|
async function deleteFile(path) {
|
|
if (!confirm('Are you sure you want to delete this item?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/drive/file`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path })
|
|
});
|
|
|
|
if (response.ok) {
|
|
showNotification('Deleted successfully', 'success');
|
|
loadFiles(currentPath);
|
|
} else {
|
|
showNotification('Failed to delete', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete error:', error);
|
|
showNotification('Error deleting item', 'error');
|
|
}
|
|
}
|
|
|
|
// Show notification
|
|
function showNotification(message, type = 'success') {
|
|
const notification = document.getElementById('notification');
|
|
notification.textContent = message;
|
|
notification.className = `notification ${type} show`;
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeUploadModal();
|
|
closeFolderModal();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|