Initial commit: Cloud Control Panel
This commit is contained in:
2304
public/assets/cloud.css
Executable file
2304
public/assets/cloud.css
Executable file
File diff suppressed because it is too large
Load Diff
144
public/index.php
Executable file
144
public/index.php
Executable file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use Din9xtrCloud\App;
|
||||
use Din9xtrCloud\Container\Container;
|
||||
use Din9xtrCloud\Controllers\AuthController;
|
||||
use Din9xtrCloud\Controllers\DashboardController;
|
||||
use Din9xtrCloud\Controllers\LicenseController;
|
||||
use Din9xtrCloud\Controllers\StorageController;
|
||||
use Din9xtrCloud\Controllers\StorageTusController;
|
||||
use Din9xtrCloud\Middlewares\AuthMiddleware;
|
||||
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
||||
use Din9xtrCloud\Router;
|
||||
use Din9xtrCloud\Storage\Drivers\LocalStorageDriver;
|
||||
use Din9xtrCloud\Storage\Drivers\StorageDriverInterface;
|
||||
use Din9xtrCloud\Storage\UserStorageInitializer;
|
||||
use Din9xtrCloud\ViewModels\BaseViewModel;
|
||||
use Din9xtrCloud\ViewModels\LayoutConfig;
|
||||
use FastRoute\RouteCollector;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Monolog\Level;
|
||||
use Nyholm\Psr7Server\ServerRequestCreator;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// PHP runtime
|
||||
// ---------------------------------------------------------------------
|
||||
error_reporting(E_ALL);
|
||||
|
||||
session_start();
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ENV
|
||||
// ---------------------------------------------------------------------
|
||||
$storageBasePath = dirname(__DIR__) . '/' . ($_ENV['STORAGE_PATH'] ?? 'storage');
|
||||
$userLimitBytes = (int)($_ENV['STORAGE_USER_LIMIT_GB'] ?? 70) * 1024 * 1024 * 1024;
|
||||
// ---------------------------------------------------------------------
|
||||
// Container
|
||||
// ---------------------------------------------------------------------
|
||||
$container = new Container();
|
||||
|
||||
$logPath = dirname(__DIR__) . '/logs/cloud.log';
|
||||
if (!is_dir(dirname($logPath))) mkdir(dirname($logPath), 0755, true);
|
||||
|
||||
$container->singleton(StorageDriverInterface::class, function () use ($storageBasePath, $userLimitBytes) {
|
||||
return new LocalStorageDriver(
|
||||
basePath: $storageBasePath,
|
||||
defaultLimitBytes: $userLimitBytes
|
||||
);
|
||||
});
|
||||
$container->singleton(UserStorageInitializer::class, function () use ($storageBasePath) {
|
||||
return new UserStorageInitializer($storageBasePath);
|
||||
});
|
||||
$container->singleton(LoggerInterface::class, function () use ($logPath) {
|
||||
$logger = new Logger('cloud');
|
||||
$logger->pushHandler(new StreamHandler($logPath, Level::Debug));
|
||||
$logger->pushProcessor(new PsrLogMessageProcessor());
|
||||
return $logger;
|
||||
});
|
||||
$container->singleton(PDO::class, function () {
|
||||
return new PDO(
|
||||
'sqlite:/var/db/database.sqlite',
|
||||
null,
|
||||
null,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
});
|
||||
$container->singleton(Psr17Factory::class, fn() => new Psr17Factory());
|
||||
|
||||
$container->request(
|
||||
ServerRequestInterface::class,
|
||||
fn(Container $c) => new ServerRequestCreator(
|
||||
$c->get(Psr17Factory::class),
|
||||
$c->get(Psr17Factory::class),
|
||||
$c->get(Psr17Factory::class),
|
||||
$c->get(Psr17Factory::class),
|
||||
)->fromGlobals()
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Routes
|
||||
// ---------------------------------------------------------------------
|
||||
$routes = static function (RouteCollector $r): void {
|
||||
|
||||
$r->get('/', [DashboardController::class, 'index']);
|
||||
$r->get('/license', [LicenseController::class, 'license']);
|
||||
|
||||
$r->get('/login', [AuthController::class, 'loginForm']);
|
||||
$r->post('/login', [AuthController::class, 'loginSubmit']);
|
||||
$r->post('/logout', [AuthController::class, 'logout']);
|
||||
|
||||
$r->post('/storage/folders', [StorageController::class, 'createFolder']);
|
||||
$r->post('/storage/files', [StorageController::class, 'uploadFile']);
|
||||
|
||||
$r->get('/folders/{folder}', [StorageController::class, 'showFolder']);
|
||||
|
||||
$r->post('/storage/folders/{folder}/delete', [StorageController::class, 'deleteFolder']);
|
||||
|
||||
$r->addRoute(['POST', 'OPTIONS'], '/storage/tus', [
|
||||
StorageTusController::class,
|
||||
'handle',
|
||||
]);
|
||||
$r->patch('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'patch']);
|
||||
$r->head('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'head']);
|
||||
|
||||
$r->get('/storage/files/download', [StorageController::class, 'downloadFile']);
|
||||
$r->post('/storage/files/delete', [StorageController::class, 'deleteFile']);
|
||||
|
||||
$r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']);
|
||||
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
|
||||
};
|
||||
|
||||
|
||||
// route,middlewares
|
||||
$router = new Router($routes, $container);
|
||||
//$router->middlewareFor('/', AuthMiddleware::class);
|
||||
//$router->middlewareFor('/login', AuthMiddleware::class);
|
||||
|
||||
|
||||
$app = new App($container);
|
||||
|
||||
//global,middlewares
|
||||
$app->middleware(
|
||||
CsrfMiddleware::class,
|
||||
AuthMiddleware::class
|
||||
);
|
||||
$app->router($router);
|
||||
|
||||
try {
|
||||
$container->beginRequest();
|
||||
$app->dispatch();
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Internal Server Error';
|
||||
}
|
||||
60
public/js/dashboard.js
Normal file
60
public/js/dashboard.js
Normal file
@@ -0,0 +1,60 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const {modalManager, FileUploader, showNotification} = window.sharedUtils;
|
||||
|
||||
/* =======================
|
||||
* ELEMENTS
|
||||
* ======================= */
|
||||
const folderModal = document.getElementById('create-folder-modal');
|
||||
const uploadModal = document.getElementById('upload-file-modal');
|
||||
|
||||
const folderOpenBtn = document.getElementById('create-folder-btn');
|
||||
const uploadOpenBtn = document.getElementById('upload-file-btn');
|
||||
|
||||
const folderCloseBtn = document.getElementById('cancel-create-folder');
|
||||
const uploadCloseBtn = document.getElementById('cancel-upload-file');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (folderModal) folderModal.style.display = 'none';
|
||||
if (uploadModal) uploadModal.style.display = 'none';
|
||||
|
||||
if (uploadModal) {
|
||||
new FileUploader({
|
||||
formSelector: '#upload-file-modal form',
|
||||
fileInputSelector: '#upload-file-modal input[type="file"]',
|
||||
folderSelectSelector: '#upload-file-modal select[name="folder"]',
|
||||
progressFillSelector: '#upload-file-modal .progress-fill',
|
||||
progressTextSelector: '#upload-file-modal .progress-text',
|
||||
uploadProgressSelector: '#upload-file-modal .upload-progress',
|
||||
submitBtnSelector: '#upload-file-modal #submit-upload',
|
||||
cancelBtnSelector: '#upload-file-modal #cancel-upload-file'
|
||||
});
|
||||
}
|
||||
|
||||
if (folderOpenBtn) {
|
||||
folderOpenBtn.addEventListener('click', () => modalManager.open(folderModal));
|
||||
}
|
||||
|
||||
if (uploadOpenBtn) {
|
||||
uploadOpenBtn.addEventListener('click', () => modalManager.open(uploadModal));
|
||||
}
|
||||
|
||||
if (folderCloseBtn) {
|
||||
folderCloseBtn.addEventListener('click', () => modalManager.close());
|
||||
}
|
||||
|
||||
if (uploadCloseBtn) {
|
||||
uploadCloseBtn.addEventListener('click', () => modalManager.close());
|
||||
}
|
||||
|
||||
[folderModal, uploadModal].forEach(modal => {
|
||||
if (modal) {
|
||||
modal.addEventListener('click', e => {
|
||||
if (e.target === modal) modalManager.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
229
public/js/folder.js
Normal file
229
public/js/folder.js
Normal file
@@ -0,0 +1,229 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const {modalManager, FileUploader, showNotification} = window.sharedUtils;
|
||||
|
||||
/* =======================
|
||||
* SELECT/DELETE FILES LOGIC
|
||||
* ======================= */
|
||||
class FileSelectionManager {
|
||||
constructor() {
|
||||
this.selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||
this.fileCheckboxes = document.querySelectorAll('.file-select-checkbox');
|
||||
this.fileCards = document.querySelectorAll('.file-card');
|
||||
this.deleteMultipleForm = document.querySelector('.multiple-delete-form');
|
||||
this.downloadMultipleForm = document.querySelector('.multiple-download-form');
|
||||
this.deleteMultipleBtn = document.getElementById('delete-multiple-btn');
|
||||
this.downloadMultipleBtn = document.getElementById('download-multiple-btn');
|
||||
this.multipleFileNamesInput = document.getElementById('multiple-file-names');
|
||||
this.multipleDownloadNamesInput = document.getElementById('multiple-download-names');
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.selectAllCheckbox) {
|
||||
this.selectAllCheckbox.addEventListener('change', () => this.toggleSelectAll());
|
||||
}
|
||||
|
||||
this.fileCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => this.updateSelectionState());
|
||||
});
|
||||
|
||||
this.fileCards.forEach(card => {
|
||||
card.addEventListener('click', (e) => this.handleCardClick(e, card));
|
||||
});
|
||||
|
||||
if (this.deleteMultipleForm) {
|
||||
this.deleteMultipleForm.addEventListener('submit', (e) => this.handleMultipleDelete(e));
|
||||
}
|
||||
|
||||
if (this.downloadMultipleForm) {
|
||||
this.downloadMultipleForm.addEventListener('submit', (e) => this.handleMultipleDownload(e));
|
||||
}
|
||||
}
|
||||
|
||||
handleCardClick(e, card) {
|
||||
if (e.target.closest('.file-card-actions') ||
|
||||
e.target.closest('.file-action-btn') ||
|
||||
e.target.closest('.delete-form') ||
|
||||
e.target.closest('a')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkbox = card.querySelector('.file-select-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const isChecked = this.selectAllCheckbox.checked;
|
||||
this.fileCheckboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
this.updateSelectionState();
|
||||
}
|
||||
|
||||
updateSelectionState() {
|
||||
const selectedFiles = Array.from(this.fileCheckboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.value);
|
||||
|
||||
// flash selected
|
||||
this.fileCards.forEach(card => {
|
||||
const fileName = card.dataset.fileName;
|
||||
if (selectedFiles.includes(fileName)) {
|
||||
card.classList.add('selected');
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// show/hide action-buttons
|
||||
if (selectedFiles.length > 0) {
|
||||
this.deleteMultipleForm.style.display = 'block';
|
||||
this.downloadMultipleForm.style.display = 'inline-block';
|
||||
this.multipleFileNamesInput.value = JSON.stringify(selectedFiles);
|
||||
this.multipleDownloadNamesInput.value = JSON.stringify(selectedFiles);
|
||||
|
||||
this.deleteMultipleBtn.innerHTML = `
|
||||
<span class="btn-icon">🗑️</span>
|
||||
Delete Selected (${selectedFiles.length})
|
||||
`;
|
||||
this.downloadMultipleBtn.innerHTML = `
|
||||
<span class="btn-icon">⬇️</span>
|
||||
Download Selected (${selectedFiles.length})
|
||||
`;
|
||||
} else {
|
||||
this.deleteMultipleForm.style.display = 'none';
|
||||
this.downloadMultipleForm.style.display = 'none';
|
||||
}
|
||||
|
||||
// refresh select all
|
||||
if (this.selectAllCheckbox) {
|
||||
this.selectAllCheckbox.checked = selectedFiles.length === this.fileCheckboxes.length && this.fileCheckboxes.length > 0;
|
||||
this.selectAllCheckbox.indeterminate = selectedFiles.length > 0 && selectedFiles.length < this.fileCheckboxes.length;
|
||||
}
|
||||
}
|
||||
|
||||
handleMultipleDelete(e) {
|
||||
if (!confirm('Are you sure you want to delete selected files?')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
handleMultipleDownload(e) {
|
||||
console.log('Downloading selected files...');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* FILE PREVIEW
|
||||
* ======================= */
|
||||
class FilePreviewManager {
|
||||
constructor(fileInput, previewContainer) {
|
||||
this.fileInput = fileInput;
|
||||
this.previewContainer = previewContainer;
|
||||
this.img = previewContainer.querySelector('img');
|
||||
this.info = previewContainer.querySelector('.file-info');
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.fileInput.addEventListener('change', () => this.updatePreview());
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
const file = this.fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
this.previewContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
this.previewContainer.style.display = 'block';
|
||||
this.info.innerHTML = `
|
||||
<div><strong>Name:</strong> ${file.name}</div>
|
||||
<div><strong>Size:</strong> ${formatBytes(file.size)}</div>
|
||||
<div><strong>Type:</strong> ${file.type || 'Unknown'}</div>
|
||||
`;
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.img.src = e.target.result;
|
||||
this.img.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
this.img.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.previewContainer.style.display = 'none';
|
||||
this.img.src = '';
|
||||
this.img.style.display = 'none';
|
||||
this.info.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* Load init
|
||||
* ======================= */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadModal = document.getElementById('upload-file-modal-folder');
|
||||
if (uploadModal) {
|
||||
uploadModal.style.display = 'none';
|
||||
|
||||
new FileUploader({
|
||||
formSelector: '#upload-form-folder',
|
||||
fileInputSelector: '#file-input-folder',
|
||||
progressFillSelector: '#upload-file-modal-folder .progress-fill',
|
||||
progressTextSelector: '#upload-file-modal-folder .progress-text',
|
||||
uploadProgressSelector: '#upload-file-modal-folder .upload-progress',
|
||||
submitBtnSelector: '#upload-file-modal-folder #submit-upload-folder',
|
||||
cancelBtnSelector: '#upload-file-modal-folder #cancel-upload-file-folder',
|
||||
onSuccess: () => {
|
||||
showNotification('File uploaded successfully', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById('file-input-folder');
|
||||
const previewContainer = uploadModal.querySelector('.file-preview');
|
||||
if (fileInput && previewContainer) {
|
||||
new FilePreviewManager(fileInput, previewContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.querySelector('.file-select-checkbox')) {
|
||||
new FileSelectionManager();
|
||||
}
|
||||
|
||||
const uploadBtn = document.getElementById('upload-file-folder');
|
||||
const uploadFirstBtn = document.getElementById('upload-first-file');
|
||||
const cancelUploadBtn = document.getElementById('cancel-upload-file-folder');
|
||||
|
||||
const openUploadModal = () => modalManager.open(uploadModal);
|
||||
const closeUploadModal = () => modalManager.close();
|
||||
|
||||
if (uploadBtn) uploadBtn.addEventListener('click', openUploadModal);
|
||||
if (uploadFirstBtn) uploadFirstBtn.addEventListener('click', openUploadModal);
|
||||
if (cancelUploadBtn) cancelUploadBtn.addEventListener('click', closeUploadModal);
|
||||
|
||||
if (uploadModal) {
|
||||
uploadModal.addEventListener('click', e => {
|
||||
if (e.target === uploadModal) closeUploadModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
363
public/js/shared.js
Normal file
363
public/js/shared.js
Normal file
@@ -0,0 +1,363 @@
|
||||
// /js/shared.js
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* =======================
|
||||
* CONFIG
|
||||
* ======================= */
|
||||
const SMALL_FILE_LIMIT = 256 * 1024 * 1024; // 256MB
|
||||
const TUS_ENDPOINT = '/storage/tus';
|
||||
|
||||
/* =======================
|
||||
* STATE
|
||||
* ======================= */
|
||||
let currentUploadType = '';
|
||||
|
||||
/* =======================
|
||||
* UTILS
|
||||
* ======================= */
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 Bytes';
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getErrorMessage(code) {
|
||||
const errors = {
|
||||
no_file: 'No file selected',
|
||||
upload_failed: 'Upload failed',
|
||||
storage_limit: 'Storage limit exceeded',
|
||||
invalid_folder: 'Invalid folder selected'
|
||||
};
|
||||
return errors[code] || 'Unknown error';
|
||||
}
|
||||
|
||||
function getSuccessMessage(code) {
|
||||
const success = {
|
||||
file_uploaded: 'File uploaded successfully',
|
||||
folder_created: 'Folder created successfully',
|
||||
files_deleted: 'Files deleted successfully'
|
||||
};
|
||||
return success[code] || 'Success';
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* NOTIFICATIONS
|
||||
* ======================= */
|
||||
window.showNotification = function (message, type = 'info') {
|
||||
const body = document.body;
|
||||
const notification = document.createElement('div');
|
||||
|
||||
const oldNotifications = document.querySelectorAll('.global-notification');
|
||||
oldNotifications.forEach(n => n.remove());
|
||||
|
||||
notification.className = 'global-notification';
|
||||
notification.textContent = message;
|
||||
|
||||
const bgColor = type === 'error' ? '#e53e3e' :
|
||||
type === 'success' ? '#38a169' :
|
||||
type === 'warning' ? '#d69e2e' : '#3182ce';
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
background: ${bgColor};
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-in';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
|
||||
if (!document.querySelector('#notification-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'notification-styles';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
/* =======================
|
||||
* MODAL MANAGEMENT
|
||||
* ======================= */
|
||||
window.modalManager = {
|
||||
currentModal: null,
|
||||
|
||||
open(modal) {
|
||||
this.closeAll();
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
this.currentModal = modal;
|
||||
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') this.close();
|
||||
};
|
||||
modal._escHandler = escHandler;
|
||||
document.addEventListener('keydown', escHandler);
|
||||
},
|
||||
|
||||
close() {
|
||||
if (this.currentModal) {
|
||||
document.removeEventListener('keydown', this.currentModal._escHandler);
|
||||
this.currentModal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
this.currentModal = null;
|
||||
}
|
||||
},
|
||||
|
||||
closeAll() {
|
||||
document.querySelectorAll('.modal-overlay').forEach(modal => {
|
||||
modal.style.display = 'none';
|
||||
if (modal._escHandler) {
|
||||
document.removeEventListener('keydown', modal._escHandler);
|
||||
}
|
||||
});
|
||||
document.body.style.overflow = '';
|
||||
this.currentModal = null;
|
||||
}
|
||||
};
|
||||
|
||||
/* =======================
|
||||
* FILE UPLOAD
|
||||
* ======================= */
|
||||
class FileUploader {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
formSelector: '',
|
||||
fileInputSelector: '',
|
||||
folderSelectSelector: null,
|
||||
progressFillSelector: '.progress-fill',
|
||||
progressTextSelector: '.progress-text',
|
||||
uploadProgressSelector: '.upload-progress',
|
||||
submitBtnSelector: '',
|
||||
cancelBtnSelector: '',
|
||||
onSuccess: null,
|
||||
onError: null,
|
||||
...options
|
||||
};
|
||||
|
||||
this.form = document.querySelector(this.options.formSelector);
|
||||
this.fileInput = document.querySelector(this.options.fileInputSelector);
|
||||
this.folderSelect = this.options.folderSelectSelector ?
|
||||
document.querySelector(this.options.folderSelectSelector) : null;
|
||||
this.progressFill = document.querySelector(this.options.progressFillSelector);
|
||||
this.progressText = document.querySelector(this.options.progressTextSelector);
|
||||
this.uploadProgress = document.querySelector(this.options.uploadProgressSelector);
|
||||
this.submitBtn = document.querySelector(this.options.submitBtnSelector);
|
||||
this.cancelBtn = document.querySelector(this.options.cancelBtnSelector);
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (this.form) {
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
}
|
||||
|
||||
if (this.fileInput) {
|
||||
this.fileInput.addEventListener('change', () => this.handleFileSelect());
|
||||
}
|
||||
|
||||
if (this.cancelBtn) {
|
||||
this.cancelBtn.addEventListener('click', () => this.reset());
|
||||
}
|
||||
}
|
||||
|
||||
handleFileSelect() {
|
||||
const file = this.fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
console.log('File selected:', file.name, formatBytes(file.size));
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const file = this.fileInput.files[0];
|
||||
const folder = this.folderSelect ? this.folderSelect.value : this.form.querySelector('input[name="folder"]')?.value;
|
||||
|
||||
if (!file) {
|
||||
showNotification('Please select a file', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.folderSelect && !folder) {
|
||||
showNotification('Please select a folder', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setUploadingState(true);
|
||||
|
||||
if (file.size <= SMALL_FILE_LIMIT) {
|
||||
await this.uploadSmallFile(file, folder);
|
||||
} else {
|
||||
this.uploadLargeFile(file, folder);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadSmallFile(file, folder) {
|
||||
try {
|
||||
this.updateProgress(0, 'Starting upload...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (folder) formData.append('folder', folder);
|
||||
formData.append('_csrf', this.form.querySelector('input[name="_csrf"]')?.value || '');
|
||||
|
||||
let progress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
if (progress < 90) {
|
||||
progress += 10;
|
||||
this.updateProgress(progress);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const response = await fetch('/storage/files', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
this.updateProgress(100, 'Upload complete!');
|
||||
this.onUploadSuccess();
|
||||
|
||||
} catch (error) {
|
||||
this.onUploadError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
uploadLargeFile(file, folder) {
|
||||
this.updateProgress(0, 'Starting large file upload...');
|
||||
|
||||
const upload = new tus.Upload(file, {
|
||||
endpoint: TUS_ENDPOINT,
|
||||
chunkSize: 5 * 1024 * 1024,
|
||||
retryDelays: [0, 1000, 3000, 5000],
|
||||
metadata: {
|
||||
folder: folder || '',
|
||||
filename: file.name
|
||||
},
|
||||
withCredentials: true,
|
||||
|
||||
onProgress: (uploaded, total) => {
|
||||
const percent = Math.round((uploaded / total) * 100);
|
||||
this.updateProgress(percent, `${percent}%`);
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
this.updateProgress(100, 'Upload complete!');
|
||||
setTimeout(() => this.onUploadSuccess(), 500);
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
this.onUploadError(error.toString());
|
||||
}
|
||||
});
|
||||
|
||||
upload.start();
|
||||
}
|
||||
|
||||
updateProgress(percent, text = '') {
|
||||
if (this.progressFill) {
|
||||
this.progressFill.style.width = percent + '%';
|
||||
}
|
||||
if (this.progressText) {
|
||||
this.progressText.textContent = text || percent + '%';
|
||||
}
|
||||
}
|
||||
|
||||
onUploadSuccess() {
|
||||
showNotification('File uploaded successfully', 'success');
|
||||
this.setUploadingState(false);
|
||||
modalManager.close();
|
||||
this.reset();
|
||||
|
||||
if (this.options.onSuccess) {
|
||||
this.options.onSuccess();
|
||||
} else {
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onUploadError(error) {
|
||||
showNotification(error, 'error');
|
||||
this.setUploadingState(false);
|
||||
this.reset();
|
||||
|
||||
if (this.options.onError) {
|
||||
this.options.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadingState(uploading) {
|
||||
if (this.submitBtn) this.submitBtn.disabled = uploading;
|
||||
if (this.cancelBtn) this.cancelBtn.disabled = uploading;
|
||||
if (this.uploadProgress) {
|
||||
this.uploadProgress.style.display = uploading ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.form) this.form.reset();
|
||||
this.updateProgress(0);
|
||||
this.setUploadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* INIT URL PARAMS
|
||||
* ======================= */
|
||||
function initUrlParams() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('error')) {
|
||||
showNotification(getErrorMessage(params.get('error')), 'error');
|
||||
}
|
||||
if (params.get('success')) {
|
||||
showNotification(getSuccessMessage(params.get('success')), 'success');
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* EXPORTS
|
||||
* ======================= */
|
||||
window.sharedUtils = {
|
||||
formatBytes,
|
||||
getErrorMessage,
|
||||
getSuccessMessage,
|
||||
showNotification,
|
||||
modalManager,
|
||||
FileUploader,
|
||||
initUrlParams,
|
||||
SMALL_FILE_LIMIT,
|
||||
TUS_ENDPOINT
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initUrlParams);
|
||||
|
||||
})();
|
||||
4989
public/js/tus.js
Normal file
4989
public/js/tus.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user