script.js

48.89 KB
31/07/2025 13:09
JS
script.js
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();
});