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 = `
${tag.name} `; 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 = `
${tag.name}
`; 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 = `

${album.title || `Album ${album.id}`}

${album.photo_count || 0} photos

`; 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 = `

📡 RSS Feed Preview

${selectedCount} albums selected with ${selectedPhotos} total photos

Feed URL: ${feedUrl}

💡 Only photos from selected albums will appear in the RSS feed. Each photo will include a direct link to its album.
`; } else { statsDisplay.innerHTML = `

⚠️ No albums selected for RSS feed

Select at least one album to enable RSS feed generation.
`; } } 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 = `

${album.title || `Album ${album.id}`}

${album.photo_count || 0} photos

`; 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, '
'); // 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 = `
${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 ? `
` : `
`; card.innerHTML = `
${adminActions}

${album.title || `Album ${album.id}`}

${album.photo_count || album.photoCount || 0} photos

${this.renderAlbumTags(album.tags || [])}
`; 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 `${tag.name}`; }).filter(Boolean); return tagElements.length > 0 ? `
${tagElements.join('')}
` : ''; } // 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(); });