Files
sst_site/views/admin/media.ejs
2025-10-26 14:44:10 +09:00

783 lines
38 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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Медиа Галерея - SmartSolTech Admin</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/fixes.css">
</head>
<body class="bg-gray-100">
<!-- Main Content -->
<main class="flex-1 p-8">
<div class="space-y-6">
<!-- Header -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-images mr-3 text-blue-600"></i>
Медиа Галерея
</h1>
<p class="mt-2 text-gray-600">Управление изображениями и файлами сайта</p>
</div>
<div class="flex space-x-3">
<button id="refresh-btn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-sync-alt mr-2"></i>
Обновить
</button>
<button id="upload-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
<i class="fas fa-upload mr-2"></i>
Загрузить файлы
</button>
</div>
</div>
</div>
<!-- Upload Zone -->
<div id="upload-zone" class="bg-white shadow rounded-lg p-8 border-2 border-dashed border-gray-300 text-center" style="display: none;">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt text-6xl text-gray-400 mb-4"></i>
<p class="text-xl text-gray-600 mb-2">Перетащите файлы сюда или нажмите для выбора</p>
<p class="text-gray-500">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="hidden">
<button type="button" onclick="document.getElementById('file-input').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg">
Выбрать файлы
</button>
<button id="cancel-upload" class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg ml-3">
Отмена
</button>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="bg-white shadow rounded-lg p-6" style="display: none;">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Загрузка файлов</h3>
<div class="space-y-3" id="progress-list">
<!-- Progress items will be added here -->
</div>
</div>
<!-- Filter and Search -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div class="flex space-x-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Тип файла</label>
<select id="file-type-filter" class="border border-gray-300 rounded-lg px-3 py-2">
<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>
<label class="block text-sm font-medium text-gray-700 mb-1">Размер</label>
<select id="size-filter" class="border border-gray-300 rounded-lg px-3 py-2">
<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>
<label class="block text-sm font-medium text-gray-700 mb-1">Поиск</label>
<div class="relative">
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="border border-gray-300 rounded-lg px-3 py-2 pr-10 w-64">
<i class="fas fa-search absolute right-3 top-3 text-gray-400"></i>
</div>
</div>
</div>
</div>
<!-- Media Grid -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-semibold text-gray-900">Файлы</h3>
<div class="flex items-center space-x-4">
<span id="file-count" class="text-sm text-gray-600">Загрузка...</span>
<div class="flex space-x-2">
<button id="grid-view" class="p-2 text-gray-600 hover:text-gray-900 border border-gray-300 rounded">
<i class="fas fa-th-large"></i>
</button>
<button id="list-view" class="p-2 text-gray-600 hover:text-gray-900 border border-gray-300 rounded">
<i class="fas fa-list"></i>
</button>
</div>
</div>
</div>
<!-- Loading State -->
<div id="loading" class="text-center py-12">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p class="mt-2 text-gray-600">Загрузка медиа файлов...</p>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-12" style="display: none;">
<i class="fas fa-images text-6xl text-gray-400 mb-4"></i>
<h3 class="text-xl font-semibold text-gray-900 mb-2">Нет загруженных файлов</h3>
<p class="text-gray-600 mb-6">Начните с загрузки ваших первых изображений</p>
<button onclick="document.getElementById('upload-btn').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg">
<i class="fas fa-upload mr-2"></i>
Загрузить файлы
</button>
</div>
<!-- Media Grid -->
<div id="media-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<!-- Media items will be loaded here -->
</div>
<!-- Media List -->
<div id="media-list" class="space-y-4" style="display: none;">
<!-- List items will be loaded here -->
</div>
<!-- Pagination -->
<div id="pagination" class="mt-8 flex justify-center" style="display: none;">
<nav class="flex space-x-2">
<button id="prev-page" class="px-3 py-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300 disabled:opacity-50">
<i class="fas fa-chevron-left"></i>
</button>
<div id="page-numbers" class="flex space-x-2">
<!-- Page numbers will be added here -->
</div>
<button id="next-page" class="px-3 py-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300 disabled:opacity-50">
<i class="fas fa-chevron-right"></i>
</button>
</nav>
</div>
</div>
</div>
</main>
</div>
<!-- Media Preview Modal -->
<div id="preview-modal" class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center" style="display: none;">
<div class="bg-white rounded-lg shadow-lg max-w-4xl max-h-[90vh] w-full mx-4 overflow-hidden">
<div class="p-4 border-b flex justify-between items-center">
<h3 id="modal-title" class="text-lg font-semibold text-gray-900">Предпросмотр файла</h3>
<button id="close-modal" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="p-6">
<div class="flex flex-col lg:flex-row space-y-6 lg:space-y-0 lg:space-x-6">
<div class="flex-1">
<img id="modal-image" src="" alt="" class="w-full h-auto rounded-lg shadow">
</div>
<div class="w-full lg:w-80">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Имя файла</label>
<input id="modal-filename" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2" readonly>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">URL</label>
<div class="flex">
<input id="modal-url" type="text" class="flex-1 border border-gray-300 rounded-l-lg px-3 py-2" readonly>
<button onclick="copyToClipboard()" class="bg-gray-100 border border-l-0 border-gray-300 rounded-r-lg px-3 py-2 hover:bg-gray-200">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Размер</label>
<p id="modal-size" class="text-sm text-gray-600">-</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Тип</label>
<p id="modal-type" class="text-sm text-gray-600">-</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Ширина</label>
<p id="modal-width" class="text-sm text-gray-600">-</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Высота</label>
<p id="modal-height" class="text-sm text-gray-600">-</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Загружено</label>
<p id="modal-date" class="text-sm text-gray-600">-</p>
</div>
<div class="border-t pt-4 space-y-3">
<button onclick="downloadFile()" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
<i class="fas fa-download mr-2"></i>
Скачать
</button>
<button onclick="deleteFile()" class="w-full bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg">
<i class="fas fa-trash mr-2"></i>
Удалить
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="/js/main.js"></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>