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 = `${source.name.substring( 0, 2 )}`; 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 = '
No tags available for this image.
'; 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 = 'No results found. Please try another search term.
'; } 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 = 'Found results, but none had valid images.
'; } 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 = 'Loading favorites...
You haven\'t saved any favorites yet.
'; 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 = 'You haven\'t saved any favorites yet.
'; } }, 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 = ``; 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 = ``; 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 = ``; 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'); });