Files
WaifuBoard/scripts/renderer.js

585 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
document.addEventListener('DOMContentLoaded', () => {
  const browseButton = document.getElementById('browse-button');
  const favoritesButton = document.getElementById('favorites-button');
  const settingsButton = document.getElementById('settings-button');
  const browsePage = document.getElementById('browse-page');
  const pageTitle = document.getElementById('page-title');
  const headerContext = document.getElementById('header-context');
  const searchIconButton = document.getElementById('search-icon-button');
  const searchModal = document.getElementById('search-modal');
  const searchCloseButton = document.getElementById('search-close-button');
  const searchInput = document.getElementById('search-input');
  const searchButton = document.getElementById('search-button');
  const sourceList = document.getElementById('source-list');
  const contentGallery = document.getElementById('content-gallery');
  const favoritesGallery = document.getElementById('favorites-gallery');
  const loadingSpinner = document.getElementById('loading-spinner');
 
  const infiniteLoadingSpinner = document.getElementById(
    'infinite-loading-spinner'
  );
  const messageBar = document.getElementById('message-bar');
  const galleryPlaceholder = document.getElementById('gallery-placeholder');
  const layoutRadios = document.querySelectorAll('input[name="layout"]');
  const tagInfoModal = document.getElementById('tag-info-modal');
  const tagInfoCloseButton = document.getElementById(
    'tag-info-close-button'
  );
  const tagInfoContent = document.getElementById('tag-info-content');
  let currentFavorites = [];
  let currentSource = '';
  let currentQuery = '';
  let currentLayout = 'scroll';
  let currentPage = 1;
  let isLoading = false;
  let hasNextPage = true;
  async function populateSources() {
    console.log('Requesting sources from main process...');
    const sources = await window.api.getSources();
    sourceList.innerHTML = '';  
    if (sources && sources.length > 0) {
      sources.forEach((source) => {
        const button = document.createElement('button');
        button.className =
          'source-button w-12 h-12 flex items-center justify-center rounded-xl text-gray-400 hover:bg-gray-700 hover:text-white transition-all duration-200';
        button.dataset.source = source.name;
        button.title = source.name;
        const favicon = document.createElement('img');
        favicon.className = 'w-8 h-8 rounded';
        let mainDomain = source.url;
        try {
          const hostname = new URL(source.url).hostname;
          const parts = hostname.split('.');
          if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
            mainDomain = parts.slice(1).join('.');
          } else {
            mainDomain = hostname;
          }
        } catch (e) {
          console.warn(`Could not parse domain from ${source.url}:`, e);
          mainDomain = source.name;
        }
       
        favicon.src = `https://www.google.com/s2/favicons?domain=${mainDomain}&sz=32`;
        favicon.alt = source.name;
        favicon.onerror = () => {
          button.innerHTML = `<span class="font-bold text-sm">${source.name.substring(
            0,
            2
          )}</span>`;
          favicon.remove();
        };
        button.appendChild(favicon);
        sourceList.appendChild(button);
      });
      console.log('Sources populated:', sources);
      if (sourceList.children.length > 0) {
        const firstButton = sourceList.children[0];
        firstButton.classList.add('active');
        currentSource = firstButton.dataset.source;
        updateHeader();
      }
    } else {
      console.warn('No sources were loaded from the main process.');
    }
  }
  sourceList.addEventListener('click', (e) => {
    const button = e.target.closest('.source-button');
    if (button) {
      sourceList
        .querySelectorAll('.source-button')
        .forEach((btn) => btn.classList.remove('active'));
      button.classList.add('active');
     
      currentSource = button.dataset.source;
      console.log('Source changed to:', currentSource);
      updateHeader();
      if (currentQuery) {
        performSearch();
      }
    }
  });
  function showPage(pageId) {
    document.querySelectorAll('.page').forEach((page) => {
      page.classList.add('hidden');
    });
    document.querySelectorAll('.nav-button').forEach((tab) => {
      tab.classList.remove('bg-indigo-600', 'text-white');
      tab.classList.add('text-gray-400', 'hover:bg-gray-700');
    });
    const activePage = document.getElementById(pageId);
    activePage.classList.remove('hidden');
    let activeTab;
    if (pageId === 'browse-page') {
      activeTab = browseButton;
      pageTitle.textContent = 'Browse';
      updateHeader();
    } else if (pageId === 'favorites-page') {
      activeTab = favoritesButton;
      pageTitle.textContent = 'Favorites';
      headerContext.textContent = '';
      loadFavorites();
    } else if (pageId === 'settings-page') {
      activeTab = settingsButton;
      pageTitle.textContent = 'Settings';
      headerContext.textContent = '';
    }
    activeTab.classList.add('bg-indigo-600', 'text-white');
    activeTab.classList.remove('text-gray-400', 'hover:bg-gray-700');
  }
  browseButton.addEventListener('click', () => showPage('browse-page'));
  favoritesButton.addEventListener('click', () => showPage('favorites-page'));
  settingsButton.addEventListener('click', () => showPage('settings-page'));
  searchIconButton.addEventListener('click', () => {
    searchModal.classList.remove('hidden');
    searchInput.focus();
    searchInput.select();
  });
  searchCloseButton.addEventListener('click', () => {
    searchModal.classList.add('hidden');
  });
  searchButton.addEventListener('click', () => {
    performSearch();
  });
 
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      searchModal.classList.add('hidden');
    }
  });
  tagInfoCloseButton.addEventListener('click', () => {
    tagInfoModal.classList.add('hidden');
  });
 
  tagInfoModal.addEventListener('click', (e) => {
    if (e.target === tagInfoModal) {
      tagInfoModal.classList.add('hidden');
    }
  });
  function showTagModal(tags) {
    tagInfoContent.innerHTML = '';
    if (!tags || tags.length === 0) {
      tagInfoContent.innerHTML =
        '<p class="text-gray-400">No tags available for this image.</p>';
      tagInfoModal.classList.remove('hidden');
      return;
    }
    const fragment = document.createDocumentFragment();
    tags.forEach((tag) => {
      if (tag) {
        const tagPill = document.createElement('span');
        tagPill.className =
          'px-2.5 py-1 bg-gray-700 text-gray-300 text-xs font-medium rounded-full';
        tagPill.textContent = tag.replace(/_/g, ' ');
        fragment.appendChild(tagPill);
      }
    });
    tagInfoContent.appendChild(fragment);
    tagInfoModal.classList.remove('hidden');
  }
  function updateHeader() {
    if (currentSource) {
      headerContext.textContent = `Source: ${currentSource}`;
    } else {
      headerContext.textContent = 'No source selected';
    }
  }
  async function performSearch() {
    if (!currentSource) {
      showMessage('Please select a source from the sidebar.', 'error');
      return;
    }
    currentPage = 1;
    hasNextPage = true;
    isLoading = false;
    currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' ');
    if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
    applyLayoutToGallery(contentGallery, currentLayout);
    contentGallery.innerHTML = '';
    updateHeader();
    searchModal.classList.add('hidden');
    loadMoreResults();
  }
  async function loadMoreResults() {
    if (isLoading || !hasNextPage) {
      return;
    }
    isLoading = true;
    if (currentPage === 1) {
      loadingSpinner.classList.remove('hidden');
    } else {
      infiniteLoadingSpinner.classList.remove('hidden');
    }
    const result = await window.api.search(
      currentSource,
      currentQuery,
      currentPage
    );
    loadingSpinner.classList.add('hidden');
    infiniteLoadingSpinner.classList.add('hidden');
    if (
      !result.success ||
      !result.data.results ||
      result.data.results.length === 0
    ) {
      hasNextPage = false;
      if (currentPage === 1) {
        applyLayoutToGallery(contentGallery, currentLayout);
        contentGallery.innerHTML =
          '<p class="text-gray-400 text-center text-lg">No results found. Please try another search term.</p>';
      }
      isLoading = false;
      return;
    }
    const validResults = result.data.results.filter((item) => item.image);
    if (validResults.length === 0) {
      hasNextPage = false;
      if (currentPage === 1) {
        applyLayoutToGallery(contentGallery, currentLayout);
        contentGallery.innerHTML =
          '<p class="text-gray-400 text-center text-lg">Found results, but none had valid images.</p>';
      }
      isLoading = false;
      return;
    }
    const fragment = document.createDocumentFragment();
    validResults.forEach((item) => {
      const thumbnailUrl = item.image;
      const displayUrl = item.sampleImageUrl || item.fullImageUrl || thumbnailUrl;
      const card = createImageCard(
        item.id.toString(),
        item.tags,
        displayUrl,  
        thumbnailUrl,
        'browse'
      );
      fragment.appendChild(card);
    });
    contentGallery.appendChild(fragment);
    hasNextPage = result.data.hasNextPage;
    currentPage++;
    isLoading = false;
  }
  browsePage.addEventListener('scroll', () => {
    if (
      browsePage.scrollTop + browsePage.clientHeight >=
      browsePage.scrollHeight - 600
    ) {
      loadMoreResults();
    }
  });
  async function loadFavorites() {
    applyLayoutToGallery(favoritesGallery, currentLayout);
    favoritesGallery.innerHTML =
      '<div class="text-center p-10"><p class="text-gray-400">Loading favorites...</p></div>';
    currentFavorites = await window.api.getFavorites();
    if (currentFavorites.length === 0) {
      applyLayoutToGallery(favoritesGallery, currentLayout);
      favoritesGallery.innerHTML =
        '<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
      return;
    }
    applyLayoutToGallery(favoritesGallery, currentLayout);
    favoritesGallery.innerHTML = '';
    const fragment = document.createDocumentFragment();
    currentFavorites.forEach((fav) => {
      const card = createImageCard(
        fav.id,
        fav.tags ? fav.tags.split(',') : [],
        fav.image_url,
        fav.thumbnail_url,
        'fav'
      );
      fragment.appendChild(card);
    });
    favoritesGallery.appendChild(fragment);
  }
  async function handleAddFavorite(id, tags, imageUrl, thumbnailUrl) {
    const safeTags = Array.isArray(tags) ? tags : [];
    const title = safeTags.length > 0 ? safeTags[0] : 'Favorite';
    const allTags = safeTags.join(',');
    const result = await window.api.addFavorite({
      id,
      title,
      imageUrl,
      thumbnailUrl,
      tags: allTags,
    });
    if (result.success) {
      showMessage('Added to favorites!', 'success');
    } else {
      showMessage(result.error, 'error');
    }
  }
  async function handleRemoveFavorite(id) {
    const result = await window.api.removeFavorite(id);
    if (result.success) {
      showMessage('Removed from favorites.', 'success');
      const cardToRemove = document.querySelector(
        `#favorites-gallery [data-id='${id}']`
      );
      if (cardToRemove) {
        cardToRemove.classList.add('opacity-0', 'scale-90');
        setTimeout(() => {
          cardToRemove.remove();
          if (favoritesGallery.children.length === 0) {
            applyLayoutToGallery(favoritesGallery, currentLayout);
            favoritesGallery.innerHTML =
              '<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
          }
        }, 300);
      }
    } else {
      showMessage(result.error, 'error');
    }
  }
  function createImageCard(id, tags, imageUrl, thumbnailUrl, type) {
    const safeTags = Array.isArray(tags) ? tags : [];
    const entry = document.createElement('div');
    entry.dataset.id = id;
    entry.className = `image-entry group relative bg-gray-800 rounded-lg shadow-lg overflow-hidden transition-all duration-300`;
    if (currentLayout === 'compact') {
      entry.classList.add('aspect-square');
    }
    const imageContainer = document.createElement('div');
    imageContainer.className =
      'w-full bg-gray-700 animate-pulse relative';
    if (currentLayout === 'compact') {
      imageContainer.classList.add('h-full');
    } else {
      imageContainer.classList.add('min-h-[200px]');
    }
    entry.appendChild(imageContainer);
    const img = document.createElement('img');
    img.src = imageUrl;
    img.alt = safeTags.join(', ');
    img.className = 'w-full h-auto object-contain bg-gray-900 opacity-0';
    img.loading = 'lazy';
    img.referrerPolicy = 'no-referrer';
    if (currentLayout === 'compact') {
        img.className = 'w-full h-full object-cover bg-gray-900 opacity-0';
    }
    img.onload = () => {
      imageContainer.classList.remove('animate-pulse', 'bg-gray-700');
      img.classList.remove('opacity-0');
      img.classList.add('transition-opacity', 'duration-500');
    };
    img.onerror = () => {
      console.warn(`Failed to load full image: ${imageUrl}. Falling back to thumbnail.`);
      img.src = thumbnailUrl;
      imageContainer.classList.remove('animate-pulse', 'bg-gray-700');
      img.classList.remove('opacity-0');
      img.classList.add('transition-opacity', 'duration-500');
      img.onerror = null;
    };
    imageContainer.appendChild(img);
    const buttonContainer = document.createElement('div');
    buttonContainer.className =
      'image-buttons absolute top-3 right-3 flex flex-col space-y-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
    buttonContainer.appendChild(createInfoButton(safeTags));
    if (type === 'browse') {
      buttonContainer.appendChild(
        createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl)
      );
    } else {
      buttonContainer.appendChild(createRemoveFavoriteButton(id));
    }
    imageContainer.appendChild(buttonContainer);
    return entry;
  }
  // ------------------------------------------------------------------
  // DELETED: The source-specific getFullImageUrl function is removed.
  // The extensions must now provide the correct URLs in the search result.
  // ------------------------------------------------------------------
  function createInfoButton(safeTags) {
    const button = document.createElement('button');
    button.title = 'Show Info';
    button.className =
      'p-2 rounded-full bg-black/50 text-white hover:bg-blue-600 backdrop-blur-sm transition-colors';
    button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
        <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
      </svg>`;
    button.onclick = (e) => {
      e.stopPropagation();
      showTagModal(safeTags);
    };
    return button;
  }
  function createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl) {
    const button = document.createElement('button');
    button.title = 'Add to Favorites';
    button.className =
      'p-2 rounded-full bg-black/50 text-white hover:bg-indigo-600 backdrop-blur-sm transition-colors';
    button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
        <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.31h5.518a.562.562 0 01.31.95l-4.203 3.03a.563.563 0 00-.182.53l1.501 4.87a.562.562 0 01-.82.624l-4.204-3.03a.563.563 0 00-.576 0l-4.204 3.03a.562.562 0 01-.82-.624l1.501-4.87a.563.563 0 00-.182-.53L2.498 9.87a.562.562 0 01.31-.95h5.518a.563.563 0 00.475-.31L11.48 3.5z" />
      </svg>`;
    button.onclick = (e) => {
      e.stopPropagation();
      handleAddFavorite(id, safeTags, imageUrl, thumbnailUrl);
    };
    return button;
  }
  function createRemoveFavoriteButton(id) {
    const button = document.createElement('button');
    button.title = 'Remove from Favorites';
    button.className =
      'p-2 rounded-full bg-black/50 text-white hover:bg-red-600 backdrop-blur-sm transition-colors';
    button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
        <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.578 0a48.108 48.108 0 01-3.478-.397m15.408 0l-2.147-2.147A1.125 1.125 0 0016.34 3H7.66a1.125 1.125 0 00-.795.325L4.772 5.79m14.456 0l-2.29-2.29a1.125 1.125 0 00-.795-.324H8.455a1.125 1.125 0 00-.795.324L5.37 5.79m13.84 0L20.25 7.5" />
      </svg>`;
    button.onclick = (e) => {
      e.stopPropagation();
      handleRemoveFavorite(id);
    };
    return button;
  }
  function showMessage(message, type = 'success') {
    if (!messageBar) return;
    messageBar.textContent = message;
    if (type === 'error') {
      messageBar.classList.remove('bg-green-600');
      messageBar.classList.add('bg-red-600');
    } else {
      messageBar.classList.remove('bg-red-600');
      messageBar.classList.add('bg-green-600');
    }
    messageBar.classList.remove('hidden', 'translate-y-16');
    setTimeout(() => {
      messageBar.classList.add('hidden', 'translate-y-16');
    }, 3000);
  }
  function loadSettings() {
    const savedLayout = localStorage.getItem('waifuBoardLayout') || 'scroll';
    currentLayout = savedLayout;
    const savedRadio = document.querySelector(
      `input[name="layout"][value="${savedLayout}"]`
    );
    if (savedRadio) {
      savedRadio.checked = true;
    } else {
      document.getElementById('layout-scroll').checked = true;
      currentLayout = 'scroll';
      localStorage.setItem('waifuBoardLayout', 'scroll');
    }
  }
  function handleLayoutChange(e) {
    const newLayout = e.target.value;
    localStorage.setItem('waifuBoardLayout', newLayout);
    currentLayout = newLayout;
    console.log('Layout changed to:', newLayout);
    if (browsePage.classList.contains('hidden')) {
      loadFavorites();
    } else {
      if (currentQuery) {
        performSearch();
      } else {
        applyLayoutToGallery(contentGallery, currentLayout);
      }
    }
  }
  function applyLayoutToGallery(galleryElement, layout) {
    galleryElement.className = 'p-4 w-full';
    if (layout === 'scroll') {
      galleryElement.classList.add('max-w-3xl', 'mx-auto', 'space-y-8');
    } else if (layout === 'grid') {
      galleryElement.classList.add('gallery-masonry');
    } else if (layout === 'compact') {
      galleryElement.classList.add('gallery-grid');
    }
  }
  layoutRadios.forEach((radio) => {
    radio.addEventListener('change', handleLayoutChange);
  });
  loadSettings();
  populateSources();
  showPage('browse-page');
});