class PhotoGallery {
constructor() {
this.albums = [];
this.tags = [];
this.currentAlbum = null;
this.currentPhotos = [];
this.currentSlideIndex = 0;
this.photosCache = new Map();
this.currentPage = 1;
this.photosPerPage = 20;
this.isLoading = false;
this.currentTagFilter = null;
this.config = {};
this.editingAlbum = null;
this.pendingFiles = null;
this.selectedAlbumForUpload = null;
this.isAuthenticated = false;
// Slideshow auto-play properties
this.isAutoPlaying = false;
this.autoPlayInterval = null;
this.autoPlayDuration = 4000;
this.progressInterval = null;
this.progressStartTime = 0;
this.init();
}
async init() {
await this.checkAuthStatus();
await this.loadConfig();
await this.loadTags();
await this.loadAlbums();
this.setupEventListeners();
this.renderTagFilter();
// Check for direct album link
this.checkDirectAlbumLink();
}
async loadConfig() {
try {
const response = await fetch('api.php?action=getConfig');
const data = await response.json();
if (data.success) {
this.config = data.config;
this.updateRSSDiscoveryTag();
}
} catch (error) {
console.error('Error loading config:', error);
}
}
async loadTags() {
try {
const response = await fetch('api.php?action=getTags');
const data = await response.json();
if (data.success) {
this.tags = data.data.tags;
}
} catch (error) {
console.error('Error loading tags:', error);
}
}
setupEventListeners() {
// Existing modal events
document.getElementById('close-modal').addEventListener('click', () => this.closeModal());
document.getElementById('copy-album-link-btn').addEventListener('click', () => {
if (this.currentAlbum) {
this.copyAlbumLink(this.currentAlbum.id);
}
});
document.getElementById('slideshow-close').addEventListener('click', () => this.closeSlideshowModal());
document.getElementById('slideshow-delete').addEventListener('click', () => this.deleteSlideshowPhoto());
document.getElementById('slideshow-prev').addEventListener('click', () => this.previousSlide());
document.getElementById('slideshow-next').addEventListener('click', () => this.nextSlide());
document.getElementById('play-pause-btn').addEventListener('click', () => this.toggleAutoPlay());
// Authentication controls
document.getElementById('login-btn').addEventListener('click', () => this.openLoginModal());
document.getElementById('logout-btn').addEventListener('click', () => this.logout());
// Admin controls (only work when authenticated)
document.getElementById('create-album-btn').addEventListener('click', () => this.isAuthenticated && this.openAlbumForm());
document.getElementById('manage-tags-btn').addEventListener('click', () => this.isAuthenticated && this.openTagManagement());
document.getElementById('rss-config-btn').addEventListener('click', () => this.isAuthenticated && this.openRSSConfig());
// Drag and drop upload
this.setupUploadZone();
// Modal close events
this.setupModalEvents();
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (document.getElementById('slideshow-modal').classList.contains('active')) {
if (e.key === 'ArrowLeft') this.previousSlide();
if (e.key === 'ArrowRight') this.nextSlide();
if (e.key === 'Escape') this.closeSlideshowModal();
if (e.key === ' ') {
e.preventDefault();
this.toggleAutoPlay();
}
}
if (document.getElementById('album-modal').classList.contains('active')) {
if (e.key === 'Escape') this.closeModal();
}
});
// Infinite scroll
document.getElementById('photos-grid').addEventListener('scroll', () => this.handlePhotosScroll());
}
setupUploadZone() {
const uploadZone = document.getElementById('upload-zone');
const fileInput = document.getElementById('file-input');
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('drag-over');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('drag-over');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
this.handleFileSelection(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
this.handleFileSelection(e.target.files);
});
}
setupModalEvents() {
// Album form modal
document.getElementById('close-album-form').addEventListener('click', () => this.closeAlbumForm());
document.getElementById('cancel-album-form').addEventListener('click', () => this.closeAlbumForm());
document.getElementById('album-form').addEventListener('submit', (e) => {
e.preventDefault();
this.saveAlbum();
});
// Tag management modal
document.getElementById('close-tag-management').addEventListener('click', () => this.closeTagManagement());
document.getElementById('tag-form').addEventListener('submit', (e) => {
e.preventDefault();
this.createTag();
});
// RSS config modal
document.getElementById('close-rss-config').addEventListener('click', () => this.closeRSSConfig());
document.getElementById('cancel-rss-config').addEventListener('click', () => this.closeRSSConfig());
document.getElementById('rss-config-form').addEventListener('submit', (e) => {
e.preventDefault();
this.saveRSSConfig();
});
// Album selection modal
document.getElementById('close-album-selection').addEventListener('click', () => this.closeAlbumSelection());
document.getElementById('cancel-upload').addEventListener('click', () => this.closeAlbumSelection());
// Login modal
document.getElementById('close-login').addEventListener('click', () => this.closeLoginModal());
document.getElementById('cancel-login').addEventListener('click', () => this.closeLoginModal());
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault();
this.handleLogin();
});
// Close modals when clicking outside
const modals = ['album-modal', 'slideshow-modal', 'album-form-modal', 'tag-management-modal', 'rss-config-modal', 'album-selection-modal', 'login-modal'];
modals.forEach(modalId => {
document.getElementById(modalId).addEventListener('click', (e) => {
if (e.target.id === modalId) {
this.closeAllModals();
}
});
});
}
closeAllModals() {
const modals = ['album-modal', 'slideshow-modal', 'album-form-modal', 'tag-management-modal', 'rss-config-modal', 'upload-progress-modal', 'album-selection-modal', 'login-modal'];
modals.forEach(modalId => {
document.getElementById(modalId).classList.remove('active');
});
document.body.style.overflow = '';
}
openAlbumForm(album = null) {
this.editingAlbum = album;
const modal = document.getElementById('album-form-modal');
const title = document.getElementById('album-form-title');
const form = document.getElementById('album-form');
if (album) {
title.textContent = 'Edit Album';
document.getElementById('album-title').value = album.title || '';
document.getElementById('album-description').value = album.description || '';
document.getElementById('album-rss-enabled').checked = album.is_rss_enabled || false;
// ใช้ tags หรือ tag_details หรือ [] ถ้าไม่มี
let albumTags = [];
if (album.tag_details && Array.isArray(album.tag_details)) {
// ใช้ tag_details ถ้ามี (มี id, name, color)
albumTags = album.tag_details.map(tag => tag.id);
} else if (album.tags && Array.isArray(album.tags)) {
// ใช้ tags ถ้ามี (array ของ tag IDs)
albumTags = album.tags;
}
this.renderTagSelector(albumTags);
} else {
title.textContent = 'Create Album';
form.reset();
this.renderTagSelector([]);
}
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
closeAlbumForm() {
document.getElementById('album-form-modal').classList.remove('active');
document.body.style.overflow = '';
this.editingAlbum = null;
}
renderTagSelector(selectedTags = []) {
const selector = document.getElementById('album-tag-selector');
selector.innerHTML = '';
this.tags.forEach(tag => {
const tagItem = document.createElement('div');
tagItem.className = 'tag-item';
if (selectedTags.some(selectedId => selectedId == tag.id)) {
tagItem.classList.add('selected');
}
// ไม่ใช้สีพื้นหลังของ tag ในฟอร์ม เพื่อให้เห็นการเลือกชัดเจน
tagItem.innerHTML = `
<div class="tag-color" style="background-color: ${tag.color}"></div>
<span>${tag.name}</span>
`;
tagItem.addEventListener('click', () => {
tagItem.classList.toggle('selected');
});
selector.appendChild(tagItem);
});
}
async saveAlbum() {
const form = document.getElementById('album-form');
const formData = new FormData(form);
const albumData = {
title: formData.get('title'),
description: formData.get('description'),
is_rss_enabled: formData.get('is_rss_enabled') === 'on'
};
const selectedTags = Array.from(document.querySelectorAll('#album-tag-selector .tag-item.selected'))
.map(item => {
const tagName = item.querySelector('span').textContent;
return this.tags.find(tag => tag.name === tagName)?.id;
})
.filter(id => id);
try {
let response;
let albumId;
if (this.editingAlbum) {
response = await fetch(`api.php?action=updateAlbum&albumId=${this.editingAlbum.id}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(albumData)
});
albumId = this.editingAlbum.id;
} else {
response = await fetch('api.php?action=createAlbum', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(albumData)
});
}
const data = await response.json();
if (data.success) {
// ใช้ albumId ที่ได้จากการแก้ไข หรือจากการสร้างใหม่
if (!albumId) {
// ตรวจสอบโครงสร้างของ response
if (data.data && data.data.album && data.data.album.id) {
albumId = data.data.album.id;
} else if (data.album && data.album.id) {
albumId = data.album.id;
} else {
console.error('Cannot find album ID in response:', data);
this.showError('Failed to get album ID from response');
return;
}
}
// อัปเดต tags เสมอ (แม้ว่าจะเป็น array ว่างก็ตาม) เพื่อให้สามารถลบ tags ทั้งหมดได้
const tagsResponse = await fetch(`api.php?action=updateAlbumTags&albumId=${albumId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({tagIds: selectedTags})
});
const tagsData = await tagsResponse.json();
if (!tagsData.success) {
console.error('Failed to update tags:', tagsData);
this.showError(tagsData.error || 'Failed to update album tags');
return;
}
this.closeAlbumForm();
await this.loadAlbums();
this.showSuccess(this.editingAlbum ? 'Album updated successfully' : 'Album created successfully');
} else {
console.error('Failed to save album:', data);
this.showError(data.error || 'Failed to save album');
}
} catch (error) {
console.error('Error saving album:', error);
this.showError('Error saving album: ' + error.message);
}
}
async deleteAlbum(albumId) {
if (!confirm('Are you sure you want to delete this album? This will delete all photos in the album.')) {
return;
}
try {
const response = await fetch(`api.php?action=deleteAlbum&albumId=${albumId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
await this.loadAlbums();
this.showSuccess('Album deleted successfully');
} else {
this.showError(data.error || 'Failed to delete album');
}
} catch (error) {
console.error('Error deleting album:', error);
this.showError('Error deleting album');
}
}
// Tag Management
openTagManagement() {
document.getElementById('tag-management-modal').classList.add('active');
document.body.style.overflow = 'hidden';
this.renderTagsList();
}
closeTagManagement() {
document.getElementById('tag-management-modal').classList.remove('active');
document.body.style.overflow = '';
}
renderTagsList() {
const tagsList = document.getElementById('tags-list');
tagsList.innerHTML = '';
this.tags.forEach(tag => {
const tagItem = document.createElement('div');
tagItem.id = `tag-${tag.id}`;
tagItem.className = 'tag-item-with-actions';
tagItem.innerHTML = `
<div class="tag-color" style="background-color: ${tag.color}"></div>
<span>${tag.name}</span>
<div class="tag-actions">
<button class="tag-action-btn delete" onclick="gallery.deleteTag(${tag.id})" title="Delete tag">×</button>
</div>
`;
tagItem.style.backgroundColor = tag.color + '40';
tagsList.appendChild(tagItem);
});
}
async createTag() {
const form = document.getElementById('tag-form');
const formData = new FormData(form);
const tagData = {
name: formData.get('name'),
color: formData.get('color')
};
try {
const response = await fetch('api.php?action=createTag', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(tagData)
});
const data = await response.json();
if (data.success) {
form.reset();
document.getElementById('tag-color').value = '#3498db';
await this.loadTags();
this.renderTagsList();
this.renderTagFilter();
this.showSuccess('Tag created successfully');
} else {
this.showError(data.error || 'Failed to create tag');
}
} catch (error) {
console.error('Error creating tag:', error);
this.showError('Error creating tag');
}
}
async deleteTag(tagId) {
if (!confirm('Are you sure you want to delete this tag?')) {
return;
}
try {
const response = await fetch(`api.php?action=deleteTag&tagId=${tagId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
document.getElementById(`tag-${tagId}`).remove();
this.showSuccess('Tag deleted successfully');
} else {
this.showError(data.error || 'Failed to delete tag');
}
} catch (error) {
console.error('Error deleting tag:', error);
this.showError('Error deleting tag');
}
}
// RSS Configuration
async openRSSConfig() {
document.getElementById('rss-config-modal').classList.add('active');
document.body.style.overflow = 'hidden';
document.getElementById('rss-title').value = this.config.rss.title || '';
document.getElementById('rss-description').value = this.config.rss.description || '';
document.getElementById('rss-max-items').value = this.config.rss.max_items || 4;
document.getElementById('rss-base-url').value = this.config.rss.base_url || window.location.origin;
this.renderRSSAlbumSelector();
}
closeRSSConfig() {
document.getElementById('rss-config-modal').classList.remove('active');
document.body.style.overflow = '';
}
renderRSSAlbumSelector() {
const selector = document.getElementById('rss-album-selector');
selector.innerHTML = '';
let selectedCount = 0;
let selectedPhotos = 0;
this.albums.forEach(album => {
const albumItem = document.createElement('div');
albumItem.className = 'album-selector-item';
if (album.is_rss_enabled) {
albumItem.classList.add('selected');
selectedCount++;
selectedPhotos += album.photo_count || 0;
}
albumItem.innerHTML = `
<h4>${album.title || `Album ${album.id}`}</h4>
<p>${album.photo_count || 0} photos</p>
`;
albumItem.addEventListener('click', () => {
albumItem.classList.toggle('selected');
this.updateRSSStats();
});
selector.appendChild(albumItem);
});
this.updateRSSStats();
}
updateRSSStats() {
const selectedItems = document.querySelectorAll('#rss-album-selector .album-selector-item.selected');
const selectedCount = selectedItems.length;
let selectedPhotos = 0;
selectedItems.forEach(item => {
const photosText = item.querySelector('p').textContent;
const photos = parseInt(photosText.match(/\d+/)?.[0] || 0);
selectedPhotos += photos;
});
// Update or create RSS stats display
let statsDisplay = document.getElementById('rss-stats');
if (!statsDisplay) {
statsDisplay = document.createElement('div');
statsDisplay.id = 'rss-stats';
statsDisplay.className = 'rss-stats';
const selector = document.getElementById('rss-album-selector');
selector.parentNode.insertBefore(statsDisplay, selector.nextSibling);
}
if (selectedCount > 0) {
const feedUrl = `${window.location.origin}${window.location.pathname}gallery.rss`;
statsDisplay.innerHTML = `
<div class="rss-stats-content">
<h4>📡 RSS Feed Preview</h4>
<p><strong>${selectedCount}</strong> albums selected with <strong>${selectedPhotos}</strong> total photos</p>
<p>Feed URL: <code><a href="${feedUrl}" target="_blank">${feedUrl}</a></code></p>
<small>💡 Only photos from selected albums will appear in the RSS feed. Each photo will include a direct link to its album.</small>
</div>
`;
} else {
statsDisplay.innerHTML = `
<div class="rss-stats-content">
<p><em>⚠️ No albums selected for RSS feed</em></p>
<small>Select at least one album to enable RSS feed generation.</small>
</div>
`;
}
}
async saveRSSConfig() {
const form = document.getElementById('rss-config-form');
const formData = new FormData(form);
const rssConfig = {
rss_title: formData.get('rss_title'),
rss_description: formData.get('rss_description'),
rss_max_items: parseInt(formData.get('rss_max_items')),
rss_base_url: formData.get('rss_base_url')
};
const selectedAlbums = Array.from(document.querySelectorAll('#rss-album-selector .album-selector-item.selected'))
.map(item => {
const title = item.querySelector('h4').textContent;
return this.albums.find(album => (album.title || `Album ${album.id}`) === title)?.id;
})
.filter(id => id);
try {
const response = await fetch('api.php?action=updateConfig', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(rssConfig)
});
const data = await response.json();
if (data.success) {
for (const album of this.albums) {
const shouldBeEnabled = selectedAlbums.includes(album.id);
if (album.is_rss_enabled !== shouldBeEnabled) {
await fetch(`api.php?action=updateAlbum&albumId=${album.id}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({is_rss_enabled: shouldBeEnabled})
});
}
}
this.closeRSSConfig();
await this.loadConfig();
await this.loadAlbums();
this.updateRSSDiscoveryTag();
this.showSuccess('RSS settings saved successfully');
} else {
this.showError(data.error || 'Failed to save RSS settings');
}
} catch (error) {
console.error('Error saving RSS config:', error);
this.showError('Error saving RSS config');
}
}
// File Upload
handleFileSelection(files) {
if (files.length === 0) return;
// Validate file types
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp'];
const validFiles = Array.from(files).filter(file => {
const isValid = validTypes.includes(file.type);
if (!isValid) {
console.warn('Invalid file type:', file.name, file.type);
}
return isValid;
});
if (validFiles.length === 0) {
this.showError('Please select valid image files (JPEG, PNG, GIF, WebP).');
return;
}
if (validFiles.length !== files.length) {
this.showError(`${files.length - validFiles.length} files were skipped (invalid format). Only ${validFiles.length} files will be uploaded.`);
}
this.pendingFiles = validFiles;
this.openAlbumSelection();
}
openAlbumSelection() {
document.getElementById('album-selection-modal').classList.add('active');
document.body.style.overflow = 'hidden';
this.renderAlbumSelection();
}
closeAlbumSelection() {
document.getElementById('album-selection-modal').classList.remove('active');
document.body.style.overflow = '';
this.pendingFiles = null;
this.selectedAlbumForUpload = null;
}
renderAlbumSelection() {
const grid = document.getElementById('album-selection-grid');
grid.innerHTML = '';
this.albums.forEach(album => {
const albumItem = document.createElement('div');
albumItem.className = 'album-selection-item';
if (album.photos && album.photos.length > 0) {
albumItem.style.backgroundImage = `url('albums/${album.id}/${album.photos[0].filename}')`;
}
albumItem.innerHTML = `
<h4>${album.title || `Album ${album.id}`}</h4>
<p>${album.photo_count || 0} photos</p>
`;
albumItem.addEventListener('click', () => {
this.selectedAlbumForUpload = album.id;
this.startUpload();
});
grid.appendChild(albumItem);
});
}
async startUpload() {
if (!this.pendingFiles || !this.selectedAlbumForUpload) {
this.showError('No files or album selected for upload.');
return;
}
document.getElementById('album-selection-modal').classList.remove('active');
document.getElementById('upload-progress-modal').classList.add('active');
document.body.style.overflow = 'hidden';
const progressFill = document.getElementById('upload-progress-fill');
const status = document.getElementById('upload-status');
const details = document.getElementById('upload-details');
status.textContent = 'Checking upload limits...';
details.textContent = `${this.pendingFiles.length} files selected`;
progressFill.style.width = '0%';
try {
// Get PHP upload limits
let maxSize, maxFiles;
try {
const limitsResponse = await fetch('check-upload-limits.php');
const limitsData = await limitsResponse.json();
if (limitsData.success) {
maxSize = limitsData.limits.upload_max_filesize;
maxFiles = limitsData.limits.max_file_uploads;
} else {
throw new Error('Failed to get limits');
}
} catch (e) {
console.warn('Could not get PHP limits, using defaults:', e);
// Fallback to conservative defaults
maxSize = 2097152; // 2MB
maxFiles = 20;
}
status.textContent = 'Validating files...';
progressFill.style.width = '5%';
let totalSize = 0;
const oversizedFiles = [];
for (const file of this.pendingFiles) {
totalSize += file.size;
if (file.size > maxSize) {
oversizedFiles.push({
name: file.name,
size: this.formatFileSize(file.size),
maxSize: this.formatFileSize(maxSize)
});
}
}
if (oversizedFiles.length > 0) {
const fileList = oversizedFiles.map(f => `• ${f.name} (${f.size})`).join('\n');
throw new Error(`The following files exceed the size limit of ${this.formatFileSize(maxSize)}:\n${fileList}\n\nPlease select smaller files or compress your images.`);
}
if (this.pendingFiles.length > maxFiles) {
throw new Error(`Cannot upload more than ${maxFiles} files at once.\nYou selected ${this.pendingFiles.length} files.\n\nPlease select fewer files.`);
}
status.textContent = 'Preparing upload...';
progressFill.style.width = '10%';
const formData = new FormData();
this.pendingFiles.forEach((file, index) => {
formData.append('photos[]', file);
});
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressFill.style.width = percentComplete + '%';
status.textContent = `Uploading... ${Math.round(percentComplete)}%`;
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
status.textContent = 'Upload completed successfully!';
details.textContent = `${response.data.successful_uploads} of ${response.data.total_files} files uploaded`;
setTimeout(() => {
this.closeAllModals();
this.loadAlbums();
this.showSuccess('Photos uploaded successfully');
}, 2000);
} else {
throw new Error(response.error || response.message || 'Upload failed');
}
} catch (error) {
console.error('Error parsing response:', error, xhr.responseText);
status.textContent = 'Upload failed';
details.textContent = 'Invalid response from server';
setTimeout(() => {
this.closeAllModals();
this.showError('Upload failed: Invalid response from server');
}, 2000);
}
} else {
console.error('HTTP Error:', xhr.status, xhr.statusText, xhr.responseText);
let errorMessage = `HTTP ${xhr.status}: ${xhr.statusText}`;
// Try to parse error response for more details
try {
const errorResponse = JSON.parse(xhr.responseText);
if (errorResponse.error) {
if (typeof errorResponse.error === 'string') {
errorMessage = errorResponse.error;
} else if (errorResponse.error.message) {
errorMessage = errorResponse.error.message;
}
}
} catch (e) {
// Keep default error message if parsing fails
}
status.textContent = 'Upload failed';
details.textContent = errorMessage;
setTimeout(() => {
this.closeAllModals();
this.showError(`Upload failed: ${errorMessage}`);
}, 3000); // เพิ่มเวลาให้อ่านได้
}
});
xhr.addEventListener('error', () => {
console.error('XHR Network Error');
status.textContent = 'Upload failed';
details.textContent = 'Network error occurred. Please check your connection and try again.';
setTimeout(() => {
this.closeAllModals();
this.showError('Upload failed: Network error occurred. Please check your connection and try again.');
}, 3000);
});
xhr.open('POST', `api.php?action=uploadPhoto&albumId=${this.selectedAlbumForUpload}`);
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
status.textContent = 'Upload failed';
details.innerHTML = error.message.replace(/\n/g, '<br>'); // Support line breaks
// Close progress modal after showing error for a bit
setTimeout(() => {
this.closeAllModals();
this.showError(error.message);
}, 4000); // เพิ่มเวลาให้อ่านข้อความ error ได้
}
}
// Tag Filtering
renderTagFilter() {
const tagFilter = document.getElementById('tag-filter');
tagFilter.innerHTML = '';
const allFilter = document.createElement('div');
allFilter.className = 'tag-filter-item';
if (!this.currentTagFilter) {
allFilter.classList.add('active');
}
allFilter.textContent = 'All Albums';
allFilter.addEventListener('click', () => this.filterByTag(null));
tagFilter.appendChild(allFilter);
this.tags.forEach(tag => {
const tagFilterItem = document.createElement('div');
tagFilterItem.className = 'tag-filter-item';
if (this.currentTagFilter === tag.id) {
tagFilterItem.classList.add('active');
}
tagFilterItem.style.backgroundColor = tag.color + '40';
tagFilterItem.style.borderColor = tag.color;
tagFilterItem.innerHTML = `
<div class="tag-color" style="background-color: ${tag.color}"></div>
${tag.name}
`;
tagFilterItem.addEventListener('click', () => this.filterByTag(tag.id));
tagFilter.appendChild(tagFilterItem);
});
}
async filterByTag(tagId) {
this.currentTagFilter = tagId;
document.querySelectorAll('.tag-filter-item').forEach(item => {
item.classList.remove('active');
});
if (tagId) {
const activeFilter = Array.from(document.querySelectorAll('.tag-filter-item'))
.find(item => item.textContent.includes(this.tags.find(t => t.id === tagId)?.name));
if (activeFilter) {
activeFilter.classList.add('active');
}
} else {
document.querySelector('.tag-filter-item').classList.add('active');
}
await this.loadAlbums();
}
// Album Loading and Rendering
async loadAlbums() {
try {
let url = 'api.php?action=getAlbums';
if (this.currentTagFilter) {
url += `&tag=${this.currentTagFilter}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.success) {
this.albums = data.albums;
this.renderAlbums();
this.updateAlbumCount();
} else {
this.showError('Failed to load albums');
}
} catch (error) {
console.error('Error loading albums:', error);
this.showError('Error loading albums');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
renderAlbums() {
const albumsGrid = document.getElementById('albums-grid');
albumsGrid.innerHTML = '';
this.albums.forEach(album => {
const albumCard = this.createAlbumCard(album);
albumsGrid.appendChild(albumCard);
});
}
createAlbumCard(album) {
const card = document.createElement('div');
card.className = 'album-card';
const coverImage = album.photos && album.photos.length > 0 ? album.photos[0] : '';
const coverStyle = coverImage ? `background-image: url('albums/${album.id}/${coverImage.filename}')` : '';
const adminActions = this.isAuthenticated ? `
<div class="album-actions">
<button class="album-action-btn" onclick="gallery.openAlbumForm(gallery.albums.find(a => a.id == ${album.id}))" title="Edit album">✏️</button>
<button class="album-action-btn" onclick="gallery.deleteAlbum(${album.id})" title="Delete album">🗑️</button>
<button class="album-action-btn" onclick="event.stopPropagation(); gallery.shareAlbum(${album.id}, '${(album.title || '').replace(/'/g, '\\\'')}')" title="Share album">🔗</button>
</div>
` : `
<div class="album-actions">
<button class="album-action-btn" onclick="event.stopPropagation(); gallery.shareAlbum(${album.id}, '${(album.title || '').replace(/'/g, '\\\'')}')" title="Share album">🔗</button>
</div>
`;
card.innerHTML = `
<div class="album-cover" style="${coverStyle}"></div>
${adminActions}
<div class="album-info">
<h3 class="album-name">${album.title || `Album ${album.id}`}</h3>
<p class="album-count">${album.photo_count || album.photoCount || 0} photos</p>
${this.renderAlbumTags(album.tags || [])}
</div>
`;
card.addEventListener('click', (e) => {
if (!e.target.closest('.album-actions')) {
this.openAlbum(album);
}
});
return card;
}
renderAlbumTags(tagIds) {
if (!tagIds || tagIds.length === 0) return '';
const tagElements = tagIds.map(tagId => {
const tag = this.tags.find(t => t.id === tagId);
if (!tag) return '';
return `<span class="album-tag" style="background-color: ${tag.color}">${tag.name}</span>`;
}).filter(Boolean);
return tagElements.length > 0 ? `<div class="album-tags">${tagElements.join('')}</div>` : '';
}
// Existing photo viewing functionality
async openAlbum(album) {
this.currentAlbum = album;
this.currentPage = 1;
document.getElementById('modal-title').textContent = album.title || `Album ${album.id}`;
document.getElementById('album-modal').classList.add('active');
document.body.style.overflow = 'hidden';
document.getElementById('photos-grid').innerHTML = '';
document.getElementById('modal-loading').style.display = 'block';
await this.loadPhotos(album.id, 1);
}
// Direct album link functionality
checkDirectAlbumLink() {
const urlParams = new URLSearchParams(window.location.search);
const albumId = urlParams.get('album');
if (albumId) {
// Find the album and open it
const album = this.albums.find(a => a.id.toString() === albumId.toString());
if (album) {
setTimeout(() => {
this.openAlbum(album);
}, 100); // Small delay to ensure DOM is ready
} else {
this.showError(`Album with ID ${albumId} not found`);
}
}
}
generateAlbumLink(albumId) {
const baseUrl = window.location.origin + window.location.pathname;
return `${baseUrl}?album=${albumId}`;
}
async copyAlbumLink(albumId) {
const link = this.generateAlbumLink(albumId);
try {
await navigator.clipboard.writeText(link);
this.showSuccess('Album link copied to clipboard!');
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = link;
document.body.appendChild(textArea);
textArea.select();
textArea.setSelectionRange(0, 99999);
document.execCommand('copy');
document.body.removeChild(textArea);
this.showSuccess('Album link copied to clipboard!');
}
}
shareAlbum(albumId, albumTitle) {
const link = this.generateAlbumLink(albumId);
if (navigator.share) {
// Use Web Share API if available
navigator.share({
title: albumTitle || `Album ${albumId}`,
text: `Check out this photo album: ${albumTitle || `Album ${albumId}`}`,
url: link
}).catch(err => {
this.copyAlbumLink(albumId);
});
} else {
// Fallback to copying link
this.copyAlbumLink(albumId);
}
}
async loadPhotos(albumId, page = 1) {
if (this.isLoading) return;
const cacheKey = `${albumId}_${page}`;
if (this.photosCache.has(cacheKey)) {
const cachedData = this.photosCache.get(cacheKey);
this.renderPhotos(cachedData.photos, page === 1);
document.getElementById('modal-loading').style.display = 'none';
return;
}
this.isLoading = true;
try {
const response = await fetch(`api.php?action=getPhotos&albumId=${albumId}&page=${page}&limit=${this.photosPerPage}`);
const data = await response.json();
if (data.success) {
this.photosCache.set(cacheKey, data);
if (page === 1) {
this.currentPhotos = data.photos;
} else {
this.currentPhotos = [...this.currentPhotos, ...data.photos];
}
this.renderPhotos(data.photos, page === 1);
} else {
this.showError('Failed to load photos');
}
} catch (error) {
console.error('Error loading photos:', error);
this.showError('Error loading photos');
} finally {
this.isLoading = false;
document.getElementById('modal-loading').style.display = 'none';
}
}
renderPhotos(photos, clearGrid = false) {
const photosGrid = document.getElementById('photos-grid');
if (clearGrid) {
photosGrid.innerHTML = '';
}
photos.forEach((photo, index) => {
const photoItem = this.createPhotoItem(photo, index);
photosGrid.appendChild(photoItem);
});
}
createPhotoItem(photo, index) {
const item = document.createElement('div');
item.className = 'photo-item';
const filename = typeof photo === 'string' ? photo : photo.filename;
item.style.backgroundImage = `url('albums/${this.currentAlbum.id}/${filename}')`;
// Add delete button for authenticated users
if (this.isAuthenticated) {
const deleteBtn = document.createElement('button');
deleteBtn.className = 'photo-delete-btn';
deleteBtn.innerHTML = '🗑️';
deleteBtn.title = 'Delete photo';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deletePhoto(this.currentAlbum.id, filename, index);
});
item.appendChild(deleteBtn);
}
item.addEventListener('click', () => this.openSlideshow(index));
return item;
}
async deletePhoto(albumId, filename, photoIndex) {
if (!confirm(`Are you sure you want to delete this photo?\n\nFilename: ${filename}\n\nThis action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`api.php?action=deletePhoto&albumId=${albumId}&filename=${encodeURIComponent(filename)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
// Remove photo from current photos array
this.currentPhotos.splice(photoIndex, 1);
// Clear photos cache for this album
const cacheKeysToDelete = [];
for (let [key] of this.photosCache) {
if (key.startsWith(`${albumId}_`)) {
cacheKeysToDelete.push(key);
}
}
cacheKeysToDelete.forEach(key => this.photosCache.delete(key));
// Re-render photos grid
this.renderPhotos(this.currentPhotos, true);
// Update album photo count in the albums list
const album = this.albums.find(a => a.id == albumId);
if (album) {
album.photo_count = (album.photo_count || 0) - 1;
album.photoCount = album.photo_count; // Update both fields
}
// If we deleted the current slide in slideshow, adjust index
if (this.currentSlideIndex >= this.currentPhotos.length && this.currentPhotos.length > 0) {
this.currentSlideIndex = this.currentPhotos.length - 1;
}
// Close slideshow if no photos left
if (this.currentPhotos.length === 0) {
this.closeSlideshowModal();
this.closeModal();
} else if (document.getElementById('slideshow-modal').classList.contains('active')) {
// Update slideshow if it's open
this.updateSlideshow();
}
this.showSuccess('Photo deleted successfully');
} else {
const errorMessage = data.error?.message || data.error || 'Failed to delete photo';
this.showError(errorMessage);
}
} catch (error) {
console.error('Error deleting photo:', error);
this.showError('Error deleting photo: ' + error.message);
}
}
// Slideshow functionality
openSlideshow(startIndex) {
this.currentSlideIndex = startIndex;
document.getElementById('slideshow-modal').classList.add('active');
// Show/hide delete button based on authentication
const deleteBtn = document.getElementById('slideshow-delete');
if (this.isAuthenticated) {
deleteBtn.style.display = 'block';
} else {
deleteBtn.style.display = 'none';
}
this.updateSlideshow();
this.startAutoPlay();
}
async deleteSlideshowPhoto() {
if (this.currentPhotos.length === 0) return;
const currentPhoto = this.currentPhotos[this.currentSlideIndex];
const filename = typeof currentPhoto === 'string' ? currentPhoto : currentPhoto.filename;
await this.deletePhoto(this.currentAlbum.id, filename, this.currentSlideIndex);
}
updateSlideshow() {
const currentPhoto = this.currentPhotos[this.currentSlideIndex];
const slideshowImage = document.getElementById('slideshow-image');
const counter = document.getElementById('slideshow-counter');
const filename = typeof currentPhoto === 'string' ? currentPhoto : currentPhoto.filename;
slideshowImage.style.opacity = '0';
slideshowImage.style.transform = 'scale(0.95)';
setTimeout(() => {
slideshowImage.src = `albums/${this.currentAlbum.id}/${filename}`;
slideshowImage.onload = () => {
slideshowImage.style.opacity = '1';
slideshowImage.style.transform = 'scale(1)';
};
}, 150);
counter.textContent = `${this.currentSlideIndex + 1} / ${this.currentPhotos.length}`;
this.resetProgress();
}
previousSlide() {
this.currentSlideIndex = this.currentSlideIndex > 0
? this.currentSlideIndex - 1
: this.currentPhotos.length - 1;
this.updateSlideshow();
}
nextSlide() {
this.currentSlideIndex = this.currentSlideIndex < this.currentPhotos.length - 1
? this.currentSlideIndex + 1
: 0;
this.updateSlideshow();
}
startAutoPlay() {
this.isAutoPlaying = true;
this.updatePlayPauseButton();
this.resetProgress();
this.autoPlayInterval = setInterval(() => {
this.nextSlide();
}, this.autoPlayDuration);
}
stopAutoPlay() {
this.isAutoPlaying = false;
this.updatePlayPauseButton();
if (this.autoPlayInterval) {
clearInterval(this.autoPlayInterval);
this.autoPlayInterval = null;
}
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
}
toggleAutoPlay() {
if (this.isAutoPlaying) {
this.stopAutoPlay();
} else {
this.startAutoPlay();
}
}
updatePlayPauseButton() {
const button = document.getElementById('play-pause-btn');
button.innerHTML = this.isAutoPlaying ? '⏸️' : '▶️';
}
resetProgress() {
if (this.progressInterval) {
clearInterval(this.progressInterval);
}
if (!this.isAutoPlaying) return;
const progressBar = document.getElementById('slideshow-progress-bar');
const timer = document.getElementById('slideshow-timer');
progressBar.style.width = '0%';
this.progressStartTime = Date.now();
this.progressInterval = setInterval(() => {
const elapsed = Date.now() - this.progressStartTime;
const progress = Math.min((elapsed / this.autoPlayDuration) * 100, 100);
const remaining = Math.ceil((this.autoPlayDuration - elapsed) / 1000);
progressBar.style.width = `${progress}%`;
timer.textContent = `${remaining}s`;
if (progress >= 100) {
clearInterval(this.progressInterval);
}
}, 50);
}
closeModal() {
document.getElementById('album-modal').classList.remove('active');
document.body.style.overflow = '';
this.currentAlbum = null;
this.currentPhotos = [];
this.currentPage = 1;
}
closeSlideshowModal() {
this.stopAutoPlay();
document.getElementById('slideshow-modal').classList.remove('active');
}
handlePhotosScroll() {
const photosGrid = document.getElementById('photos-grid');
const scrollTop = photosGrid.scrollTop;
const scrollHeight = photosGrid.scrollHeight;
const clientHeight = photosGrid.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 100 && !this.isLoading) {
this.currentPage++;
this.loadPhotos(this.currentAlbum.id, this.currentPage);
}
}
updateAlbumCount() {
const albumCount = document.getElementById('album-count');
albumCount.textContent = `${this.albums.length} Albums`;
}
// RSS Discovery Tag Management
updateRSSDiscoveryTag() {
const rssLink = document.getElementById('rss-feed-link');
if (rssLink && this.config.rss_title) {
rssLink.setAttribute('title', this.config.rss_title);
rssLink.setAttribute('href', `gallery.rss`);
}
}
// Utility methods
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', '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.showToast(message, 'error');
}
showSuccess(message) {
this.showToast(message, 'success');
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
Object.assign(toast.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '15px 20px',
borderRadius: '8px',
color: 'white',
fontWeight: '500',
zIndex: '10000',
maxWidth: '300px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
transform: 'translateX(100%)',
transition: 'transform 0.3s ease'
});
if (type === 'error') {
toast.style.background = 'linear-gradient(135deg, #e74c3c, #c0392b)';
} else if (type === 'success') {
toast.style.background = 'linear-gradient(135deg, #27ae60, #229954)';
} else {
toast.style.background = 'linear-gradient(135deg, #3498db, #2980b9)';
}
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 4000);
}
// Authentication Methods
async checkAuthStatus() {
try {
const response = await fetch('api.php?action=checkAuth');
const data = await response.json();
if (data.success && data.data.authenticated) {
this.isAuthenticated = true;
this.updateAuthUI(data.data.username);
} else {
this.isAuthenticated = false;
this.updateAuthUI(null);
}
} catch (error) {
console.error('Error checking auth status:', error);
this.isAuthenticated = false;
this.updateAuthUI(null);
}
}
updateAuthUI(username) {
const loginBtn = document.getElementById('login-btn');
const userInfo = document.getElementById('user-info');
const usernameDisplay = document.getElementById('username-display');
const body = document.body;
if (username) {
loginBtn.style.display = 'none';
userInfo.style.display = 'flex';
usernameDisplay.textContent = username;
body.classList.add('authenticated');
} else {
loginBtn.style.display = 'block';
userInfo.style.display = 'none';
body.classList.remove('authenticated');
}
}
openLoginModal() {
document.getElementById('login-modal').classList.add('active');
document.body.style.overflow = 'hidden';
document.getElementById('username').focus();
}
closeLoginModal() {
document.getElementById('login-modal').classList.remove('active');
document.body.style.overflow = '';
document.getElementById('login-form').reset();
}
async handleLogin() {
const form = document.getElementById('login-form');
const formData = new FormData(form);
const loginData = {
username: formData.get('username'),
password: formData.get('password')
};
try {
const response = await fetch('auth.php?action=login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(loginData)
});
const data = await response.json();
if (data.success) {
this.isAuthenticated = true;
this.updateAuthUI(data.user);
this.closeLoginModal();
this.loadAlbums();
this.showSuccess('Login successful!');
} else {
this.showError(data.message || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
this.showError('Login failed due to network error');
}
}
async logout() {
try {
const response = await fetch('auth.php?action=logout');
const data = await response.json();
if (data.success) {
this.isAuthenticated = false;
this.updateAuthUI(null);
this.loadAlbums();
this.showSuccess('Logged out successfully');
} else {
this.showError('Logout failed');
}
} catch (error) {
console.error('Logout error:', error);
this.showError('Logout failed due to network error');
}
}
}
let gallery;
// Initialize the gallery when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
gallery = new PhotoGallery();
});