Files
sst_site/.history/views/admin/media_20251026214344.ejs
2025-10-26 22:14:47 +09:00

1251 lines
52 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-images mr-2"></i>Медиа Галерея</h1>
</div>
<div class="col-sm-6">
<div class="float-sm-right">
<button id="refresh-btn" class="btn btn-secondary">
<i class="fas fa-sync-alt mr-1"></i>Обновить
</button>
<button id="upload-btn" class="btn btn-primary">
<i class="fas fa-upload mr-1"></i>Загрузить файлы
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Upload Zone -->
<div id="upload-zone" class="card" style="display: none;">
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt fa-6x text-muted mb-4"></i>
<p class="h5 text-muted mb-2">Перетащите файлы сюда или нажмите для выбора</p>
<p class="text-muted">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="d-none">
<button type="button" onclick="document.getElementById('file-input').click()" class="btn btn-primary">
Выбрать файлы
</button>
<button id="cancel-upload" class="btn btn-secondary ml-3">
Отмена
</button>
</div>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Загрузка файлов</h3>
</div>
<div class="card-body">
<div id="progress-list">
<!-- Progress items will be added here -->
</div>
</div>
</div>
<!-- Filter and Search -->
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Тип файла</label>
<select id="file-type-filter" class="form-control">
<option value="">Все типы</option>
<option value="image/jpeg">JPEG</option>
<option value="image/png">PNG</option>
<option value="image/gif">GIF</option>
<option value="image/svg+xml">SVG</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Размер</label>
<select id="size-filter" class="form-control">
<option value="">Любой размер</option>
<option value="small">Маленький (&lt; 1MB)</option>
<option value="medium">Средний (1-5MB)</option>
<option value="large">Большой (&gt; 5MB)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Поиск</label>
<div class="input-group">
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="form-control">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-search"></i></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Media Grid -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Файлы</h3>
<div class="card-tools">
<span id="file-count" class="badge badge-secondary">Загрузка...</span>
<div class="btn-group ml-2">
<button id="grid-view" class="btn btn-sm btn-default">
<i class="fas fa-th-large"></i>
</button>
<button id="list-view" class="btn btn-sm btn-default">
<i class="fas fa-list"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- Loading State -->
<div id="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Загрузка...</span>
</div>
<p class="mt-2 text-muted">Загрузка медиа файлов...</p>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-5" style="display: none;">
<i class="fas fa-images fa-6x text-muted mb-4"></i>
<h4 class="text-muted mb-2">Нет загруженных файлов</h4>
<p class="text-muted mb-4">Начните с загрузки ваших первых изображений</p>
<button onclick="document.getElementById('upload-btn').click()" class="btn btn-primary">
<i class="fas fa-upload mr-2"></i>Загрузить файлы
</button>
</div>
<!-- Media Grid -->
<div id="media-grid" class="row">
<!-- Media items will be loaded here -->
</div>
<!-- Media List -->
<div id="media-list" style="display: none;">
<!-- List items will be loaded here -->
</div>
</div>
<!-- Pagination -->
<div class="card-footer" id="pagination" style="display: none;">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center m-0">
<li class="page-item">
<button id="prev-page" class="page-link">
<i class="fas fa-chevron-left"></i>
</button>
</li>
<div id="page-numbers" class="d-flex">
<!-- Page numbers will be added here -->
</div>
<li class="page-item">
<button id="next-page" class="page-link">
<i class="fas fa-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</section>
<!-- Media Preview Modal -->
<div class="modal fade" id="preview-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 id="modal-title" class="modal-title">Предпросмотр файла</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<img id="modal-image" src="" alt="" class="img-fluid rounded">
</div>
<div class="col-md-4">
<div class="form-group">
<label>Имя файла</label>
<input id="modal-filename" type="text" class="form-control" readonly>
</div>
<div class="form-group">
<label>URL</label>
<div class="input-group">
<input id="modal-url" type="text" class="form-control" readonly>
<div class="input-group-append">
<button onclick="copyToClipboard()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Размер</label>
<p id="modal-size" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Тип</label>
<p id="modal-type" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Ширина</label>
<p id="modal-width" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Высота</label>
<p id="modal-height" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="form-group">
<label>Загружено</label>
<p id="modal-date" class="form-control-plaintext">-</p>
</div>
<div class="btn-group d-flex">
<button onclick="downloadFile()" class="btn btn-primary">
<i class="fas fa-download mr-1"></i>Скачать
</button>
<button onclick="deleteFile()" class="btn btn-danger">
<i class="fas fa-trash mr-1"></i>Удалить
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let currentFiles = [];
let filteredFiles = [];
let currentPage = 1;
let filesPerPage = 24;
let isGridView = true;
let currentFile = null;
document.addEventListener('DOMContentLoaded', function() {
loadFiles();
setupEventListeners();
});
function setupEventListeners() {
// Upload button
document.getElementById('upload-btn').addEventListener('click', function() {
document.getElementById('upload-zone').style.display = 'block';
});
// Cancel upload
document.getElementById('cancel-upload').addEventListener('click', function() {
document.getElementById('upload-zone').style.display = 'none';
});
// File input
document.getElementById('file-input').addEventListener('change', handleFileSelect);
// Refresh
document.getElementById('refresh-btn').addEventListener('click', loadFiles);
// View toggle
document.getElementById('grid-view').addEventListener('click', function() {
isGridView = true;
updateViewButtons();
renderFiles();
});
document.getElementById('list-view').addEventListener('click', function() {
isGridView = false;
updateViewButtons();
renderFiles();
});
// Filters
document.getElementById('file-type-filter').addEventListener('change', applyFilters);
document.getElementById('size-filter').addEventListener('change', applyFilters);
document.getElementById('search-input').addEventListener('input', applyFilters);
// Drag and drop
const uploadZone = document.getElementById('upload-zone');
uploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
uploadZone.classList.add('border-blue-500');
});
uploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadZone.classList.remove('border-blue-500');
});
uploadZone.addEventListener('drop', function(e) {
e.preventDefault();
uploadZone.classList.remove('border-blue-500');
handleFileSelect({ target: { files: e.dataTransfer.files } });
});
}
function updateViewButtons() {
const gridBtn = document.getElementById('grid-view');
const listBtn = document.getElementById('list-view');
if (isGridView) {
gridBtn.classList.add('bg-blue-500', 'text-white');
gridBtn.classList.remove('btn-default');
listBtn.classList.remove('bg-blue-500', 'text-white');
listBtn.classList.add('btn-default');
} else {
listBtn.classList.add('bg-blue-500', 'text-white');
listBtn.classList.remove('btn-default');
gridBtn.classList.remove('bg-blue-500', 'text-white');
gridBtn.classList.add('btn-default');
}
}
async function loadFiles() {
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'none';
const response = await fetch('/api/admin/media');
const data = await response.json();
if (data.success) {
currentFiles = data.files;
filteredFiles = [...currentFiles];
updateFileCount();
renderFiles();
if (currentFiles.length === 0) {
document.getElementById('empty-state').style.display = 'block';
}
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error loading files:', error);
alert('Ошибка загрузки файлов: ' + error.message);
} finally {
document.getElementById('loading').style.display = 'none';
}
}
function applyFilters() {
const typeFilter = document.getElementById('file-type-filter').value;
const sizeFilter = document.getElementById('size-filter').value;
const searchQuery = document.getElementById('search-input').value.toLowerCase();
filteredFiles = currentFiles.filter(file => {
let matches = true;
// Type filter
if (typeFilter && file.mimetype !== typeFilter) {
matches = false;
}
// Size filter
if (sizeFilter) {
const sizeMB = file.size / (1024 * 1024);
if (sizeFilter === 'small' && sizeMB >= 1) matches = false;
if (sizeFilter === 'medium' && (sizeMB < 1 || sizeMB > 5)) matches = false;
if (sizeFilter === 'large' && sizeMB <= 5) matches = false;
}
// Search filter
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
matches = false;
}
return matches;
});
currentPage = 1;
updateFileCount();
renderFiles();
}
function updateFileCount() {
const countElement = document.getElementById('file-count');
countElement.textContent = `${filteredFiles.length} файлов`;
}
function renderFiles() {
const gridContainer = document.getElementById('media-grid');
const listContainer = document.getElementById('media-list');
if (isGridView) {
gridContainer.style.display = 'block';
listContainer.style.display = 'none';
renderGridView();
} else {
gridContainer.style.display = 'none';
listContainer.style.display = 'block';
renderListView();
}
renderPagination();
}
function renderGridView() {
const container = document.getElementById('media-grid');
const startIndex = (currentPage - 1) * filesPerPage;
const endIndex = startIndex + filesPerPage;
const pageFiles = filteredFiles.slice(startIndex, endIndex);
container.innerHTML = pageFiles.map(file => `
<div class="col-md-2 col-sm-3 col-6 mb-3">
<div class="card h-100">
<div class="card-img-top" style="height: 150px; background-image: url('${file.url}'); background-size: cover; background-position: center; cursor: pointer;" onclick="openPreview('${file._id}')"></div>
<div class="card-body p-2">
<p class="card-text small text-truncate" title="${file.filename}">${file.filename}</p>
<small class="text-muted">${formatFileSize(file.size)}</small>
</div>
</div>
</div>
`).join('');
}
function renderListView() {
const container = document.getElementById('media-list');
const startIndex = (currentPage - 1) * filesPerPage;
const endIndex = startIndex + filesPerPage;
const pageFiles = filteredFiles.slice(startIndex, endIndex);
container.innerHTML = pageFiles.map(file => `
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-1">
<img src="${file.url}" alt="${file.filename}" class="img-thumbnail" style="width: 60px; height: 60px; object-fit: cover;">
</div>
<div class="col-md-4">
<h6 class="mb-1">${file.filename}</h6>
<small class="text-muted">${file.mimetype}</small>
</div>
<div class="col-md-2">
<span class="badge badge-secondary">${formatFileSize(file.size)}</span>
</div>
<div class="col-md-3">
<small class="text-muted">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</small>
</div>
<div class="col-md-2 text-right">
<div class="btn-group">
<button onclick="openPreview('${file._id}')" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i>
</button>
<button onclick="downloadFile('${file._id}')" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-download"></i>
</button>
<button onclick="deleteFile('${file._id}')" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`).join('');
}
function renderPagination() {
const totalPages = Math.ceil(filteredFiles.length / filesPerPage);
const paginationContainer = document.getElementById('pagination');
if (totalPages <= 1) {
paginationContainer.style.display = 'none';
return;
}
paginationContainer.style.display = 'block';
// Update prev/next buttons
const prevBtn = document.getElementById('prev-page');
const nextBtn = document.getElementById('next-page');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages;
prevBtn.onclick = () => {
if (currentPage > 1) {
currentPage--;
renderFiles();
}
};
nextBtn.onclick = () => {
if (currentPage < totalPages) {
currentPage++;
renderFiles();
}
};
// Update page numbers
const pageNumbersContainer = document.getElementById('page-numbers');
let pageNumbers = '';
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
pageNumbers += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
} else {
pageNumbers += `<li class="page-item"><button class="page-link" onclick="goToPage(${i})">${i}</button></li>`;
}
}
pageNumbersContainer.innerHTML = pageNumbers;
}
function goToPage(page) {
currentPage = page;
renderFiles();
}
function formatFileSize(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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function openPreview(fileId) {
const file = currentFiles.find(f => f._id === fileId);
if (!file) return;
currentFile = file;
document.getElementById('modal-title').textContent = file.filename;
document.getElementById('modal-image').src = file.url;
document.getElementById('modal-filename').value = file.filename;
document.getElementById('modal-url').value = file.url;
document.getElementById('modal-size').textContent = formatFileSize(file.size);
document.getElementById('modal-type').textContent = file.mimetype;
document.getElementById('modal-width').textContent = file.width || '-';
document.getElementById('modal-height').textContent = file.height || '-';
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
$('#preview-modal').modal('show');
}
function copyToClipboard() {
const urlInput = document.getElementById('modal-url');
urlInput.select();
document.execCommand('copy');
// Show feedback
const btn = event.target.closest('button');
const originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(() => {
btn.innerHTML = originalHtml;
}, 1000);
}
function downloadFile() {
if (currentFile) {
const link = document.createElement('a');
link.href = currentFile.url;
link.download = currentFile.filename;
link.click();
}
}
async function deleteFile() {
if (!currentFile) return;
if (!confirm(`Вы уверены, что хотите удалить файл "${currentFile.filename}"?`)) {
return;
}
try {
const response = await fetch(`/api/admin/media/${currentFile._id}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
$('#preview-modal').modal('hide');
loadFiles();
alert('Файл успешно удален');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error deleting file:', error);
alert('Ошибка удаления файла: ' + error.message);
}
}
async function handleFileSelect(event) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
document.getElementById('upload-zone').style.display = 'none';
document.getElementById('upload-progress').style.display = 'block';
const progressList = document.getElementById('progress-list');
progressList.innerHTML = '';
for (let i = 0; i < files.length; i++) {
const file = files[i];
const progressItem = createProgressItem(file.name, i);
progressList.appendChild(progressItem);
try {
await uploadFile(file, i);
} catch (error) {
updateProgress(i, 100, 'error', error.message);
}
}
setTimeout(() => {
document.getElementById('upload-progress').style.display = 'none';
loadFiles();
}, 2000);
}
function createProgressItem(filename, index) {
const div = document.createElement('div');
div.className = 'progress-item';
div.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="filename">${filename}</span>
<span class="status" id="status-${index}">Загрузка...</span>
</div>
<div class="progress">
<div class="progress-bar" id="progress-${index}" role="progressbar" style="width: 0%"></div>
</div>
`;
return div;
}
function updateProgress(index, percent, status, message) {
const progressBar = document.getElementById(`progress-${index}`);
const statusSpan = document.getElementById(`status-${index}`);
if (progressBar) {
progressBar.style.width = percent + '%';
if (status === 'success') {
progressBar.classList.add('bg-success');
statusSpan.textContent = 'Готово';
statusSpan.classList.add('text-success');
} else if (status === 'error') {
progressBar.classList.add('bg-danger');
statusSpan.textContent = 'Ошибка: ' + message;
statusSpan.classList.add('text-danger');
} else {
statusSpan.textContent = Math.round(percent) + '%';
}
}
}
async function uploadFile(file, index) {
const formData = new FormData();
formData.append('file', file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
updateProgress(index, percent, 'uploading');
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
updateProgress(index, 100, 'success');
resolve(response);
} else {
reject(new Error(response.message));
}
} else {
reject(new Error('HTTP ' + xhr.status));
}
});
xhr.addEventListener('error', function() {
reject(new Error('Network error'));
});
xhr.open('POST', '/api/admin/media/upload');
xhr.send(formData);
});
}
</script>
<script>
class MediaGallery {
constructor() {
this.currentFiles = [];
this.filteredFiles = [];
this.currentView = 'grid';
this.currentPage = 1;
this.itemsPerPage = 24;
this.currentFile = null;
this.init();
}
init() {
this.setupEventListeners();
this.loadMedia();
}
setupEventListeners() {
// Upload button
document.getElementById('upload-btn').addEventListener('click', () => {
this.showUploadZone();
});
// Cancel upload
document.getElementById('cancel-upload').addEventListener('click', () => {
this.hideUploadZone();
});
// File input
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
// Refresh button
document.getElementById('refresh-btn').addEventListener('click', () => {
this.loadMedia();
});
// View toggle
document.getElementById('grid-view').addEventListener('click', () => {
this.setView('grid');
});
document.getElementById('list-view').addEventListener('click', () => {
this.setView('list');
});
// Filters
document.getElementById('file-type-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('size-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('search-input').addEventListener('input', () => {
this.applyFilters();
});
// Modal
document.getElementById('close-modal').addEventListener('click', () => {
this.closeModal();
});
// Upload zone drag and drop
const uploadZone = document.getElementById('upload-zone');
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
this.handleFiles(e.dataTransfer.files);
});
}
async loadMedia() {
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
document.getElementById('media-grid').style.display = 'none';
const response = await fetch('/api/media/list');
const data = await response.json();
if (data.success) {
this.currentFiles = data.images || [];
this.applyFilters();
} else {
throw new Error(data.message || 'Failed to load media');
}
} catch (error) {
console.error('Error loading media:', error);
this.showError('Ошибка загрузки медиа файлов');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
applyFilters() {
const typeFilter = document.getElementById('file-type-filter').value;
const sizeFilter = document.getElementById('size-filter').value;
const searchQuery = document.getElementById('search-input').value.toLowerCase();
this.filteredFiles = this.currentFiles.filter(file => {
// Type filter
if (typeFilter && file.mimetype !== typeFilter) {
return false;
}
// Size filter
if (sizeFilter) {
const sizeInMB = file.size / (1024 * 1024);
if (sizeFilter === 'small' && sizeInMB >= 1) return false;
if (sizeFilter === 'medium' && (sizeInMB < 1 || sizeInMB > 5)) return false;
if (sizeFilter === 'large' && sizeInMB <= 5) return false;
}
// Search filter
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
return false;
}
return true;
});
this.updateFileCount();
this.renderMedia();
}
updateFileCount() {
const total = this.currentFiles.length;
const filtered = this.filteredFiles.length;
const countText = filtered === total ?
`${total} файлов` :
`${filtered} из ${total} файлов`;
document.getElementById('file-count').textContent = countText;
}
renderMedia() {
if (this.filteredFiles.length === 0) {
document.getElementById('empty-state').style.display = 'block';
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'none';
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('empty-state').style.display = 'none';
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const pageFiles = this.filteredFiles.slice(startIndex, endIndex);
if (this.currentView === 'grid') {
this.renderGrid(pageFiles);
} else {
this.renderList(pageFiles);
}
this.updatePagination();
}
renderGrid(files) {
document.getElementById('media-grid').style.display = 'grid';
document.getElementById('media-list').style.display = 'none';
const grid = document.getElementById('media-grid');
grid.innerHTML = files.map(file => `
<div class="group relative bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="aspect-square">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover">
</div>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-25 transition-opacity flex items-center justify-center">
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
<button class="bg-white bg-opacity-90 text-gray-800 px-3 py-2 rounded-lg mr-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="bg-red-500 bg-opacity-90 text-white px-3 py-2 rounded-lg"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 text-white p-2">
<p class="text-xs truncate">${file.filename}</p>
<p class="text-xs text-gray-300">${this.formatFileSize(file.size)}</p>
</div>
</div>
`).join('');
}
renderList(files) {
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'block';
const list = document.getElementById('media-list');
list.innerHTML = files.map(file => `
<div class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="w-16 h-16 flex-shrink-0 mr-4">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover rounded">
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate">${file.filename}</h4>
<p class="text-sm text-gray-500">${this.formatFileSize(file.size)} • ${file.mimetype}</p>
<p class="text-xs text-gray-400">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</p>
</div>
<div class="flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 p-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="text-red-600 hover:text-red-800 p-2"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
setView(view) {
this.currentView = view;
// Update button states
document.getElementById('grid-view').classList.toggle('bg-blue-600', view === 'grid');
document.getElementById('grid-view').classList.toggle('text-white', view === 'grid');
document.getElementById('list-view').classList.toggle('bg-blue-600', view === 'list');
document.getElementById('list-view').classList.toggle('text-white', view === 'list');
this.renderMedia();
}
showUploadZone() {
document.getElementById('upload-zone').style.display = 'block';
}
hideUploadZone() {
document.getElementById('upload-zone').style.display = 'none';
document.getElementById('file-input').value = '';
}
async handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
if (!file.type.startsWith('image/')) {
this.showError(`${file.name} не является изображением`);
return false;
}
if (file.size > 10 * 1024 * 1024) {
this.showError(`${file.name} слишком большой (максимум 10MB)`);
return false;
}
return true;
});
if (validFiles.length === 0) return;
this.hideUploadZone();
await this.uploadFiles(validFiles);
}
async uploadFiles(files) {
const progressContainer = document.getElementById('upload-progress');
const progressList = document.getElementById('progress-list');
progressContainer.style.display = 'block';
progressList.innerHTML = '';
for (const file of files) {
const progressItem = this.createProgressItem(file);
progressList.appendChild(progressItem);
try {
await this.uploadSingleFile(file, progressItem);
} catch (error) {
this.updateProgressItem(progressItem, 'error', error.message);
}
}
setTimeout(() => {
progressContainer.style.display = 'none';
this.loadMedia();
}, 2000);
}
createProgressItem(file) {
const div = document.createElement('div');
div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded';
div.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas fa-image text-gray-400"></i>
<span class="text-sm text-gray-900">${file.name}</span>
<span class="text-xs text-gray-500">${this.formatFileSize(file.size)}</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-32 bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full progress-bar" style="width: 0%"></div>
</div>
<span class="text-sm text-gray-600 status">0%</span>
</div>
`;
return div;
}
updateProgressItem(item, status, message = '') {
const statusElement = item.querySelector('.status');
const progressBar = item.querySelector('.progress-bar');
if (status === 'error') {
statusElement.textContent = 'Ошибка';
statusElement.className = 'text-sm text-red-600 status';
progressBar.className = 'bg-red-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
} else if (status === 'success') {
statusElement.textContent = 'Готово';
statusElement.className = 'text-sm text-green-600 status';
progressBar.className = 'bg-green-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
}
}
async uploadSingleFile(file, progressItem) {
const formData = new FormData();
formData.append('images', file);
const xhr = new XMLHttpRequest();
const progressBar = progressItem.querySelector('.progress-bar');
const status = progressItem.querySelector('.status');
return new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
status.textContent = Math.round(percentComplete) + '%';
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.updateProgressItem(progressItem, 'success');
resolve();
} else {
reject(new Error(response.message));
}
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', '/api/media/upload-multiple');
xhr.send(formData);
});
}
openModal(filename) {
const file = this.currentFiles.find(f => f.filename === filename);
if (!file) return;
this.currentFile = file;
document.getElementById('modal-title').textContent = file.filename;
document.getElementById('modal-image').src = file.url;
document.getElementById('modal-filename').value = file.filename;
document.getElementById('modal-url').value = window.location.origin + file.url;
document.getElementById('modal-size').textContent = this.formatFileSize(file.size);
document.getElementById('modal-type').textContent = file.mimetype;
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
// Load image to get dimensions
const img = new Image();
img.onload = () => {
document.getElementById('modal-width').textContent = img.width + 'px';
document.getElementById('modal-height').textContent = img.height + 'px';
};
img.src = file.url;
document.getElementById('preview-modal').style.display = 'flex';
}
closeModal() {
document.getElementById('preview-modal').style.display = 'none';
this.currentFile = null;
}
async deleteFile(filename) {
if (!confirm(`Вы уверены, что хотите удалить файл "${filename}"?`)) {
return;
}
try {
const response = await fetch(`/api/media/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
this.showSuccess('Файл удален');
this.loadMedia();
if (this.currentFile && this.currentFile.filename === filename) {
this.closeModal();
}
} else {
throw new Error('Failed to delete file');
}
} catch (error) {
console.error('Error deleting file:', error);
this.showError('Ошибка удаления файла');
}
}
downloadFile(filename) {
const file = filename ?
this.currentFiles.find(f => f.filename === filename) :
this.currentFile;
if (!file) return;
const link = document.createElement('a');
link.href = file.url;
link.download = file.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updatePagination() {
const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage);
if (totalPages <= 1) {
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('pagination').style.display = 'block';
// Update prev/next buttons
document.getElementById('prev-page').disabled = this.currentPage === 1;
document.getElementById('next-page').disabled = this.currentPage === totalPages;
// Update page numbers
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= this.currentPage - 2 && i <= this.currentPage + 2)) {
const button = document.createElement('button');
button.className = `px-3 py-2 rounded ${
i === this.currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'
}`;
button.textContent = i;
button.onclick = () => this.goToPage(i);
pageNumbers.appendChild(button);
} else if (i === this.currentPage - 3 || i === this.currentPage + 3) {
const span = document.createElement('span');
span.className = 'px-2 py-2 text-gray-400';
span.textContent = '...';
pageNumbers.appendChild(span);
}
}
}
goToPage(page) {
this.currentPage = page;
this.renderMedia();
}
formatFileSize(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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showError(message) {
this.showNotification(message, 'error');
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// Global functions for modal
function copyToClipboard() {
const urlInput = document.getElementById('modal-url');
urlInput.select();
document.execCommand('copy');
mediaGallery.showSuccess('URL скопирован в буфер обмена');
}
function downloadFile() {
mediaGallery.downloadFile();
}
function deleteFile() {
if (mediaGallery.currentFile) {
mediaGallery.deleteFile(mediaGallery.currentFile.filename);
}
}
// Initialize
let mediaGallery;
document.addEventListener('DOMContentLoaded', () => {
mediaGallery = new MediaGallery();
});
</script>
</body>
</html>