Book Boards
+Book Sources
+ + +Library
+Select a book source above to load content
+Fetching books...
+Loading more...
+diff --git a/main.js b/main.js index c70921f..9cee976 100644 --- a/main.js +++ b/main.js @@ -73,6 +73,7 @@ function createWindow() { app.whenReady().then(() => { createWindow(); initDiscordRPC(); + headlessBrowser.init() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); @@ -93,6 +94,10 @@ const dbHandlers = require('./src/ipc/db-handlers')(db); ipcMain.handle('api:getSources', apiHandlers.getSources); ipcMain.handle('api:search', apiHandlers.search); +ipcMain.handle('api:getChapters', apiHandlers.getChapters); +ipcMain.handle('api:getPages', apiHandlers.getPages); +ipcMain.handle('api:getMetadata', apiHandlers.getMetadata); + ipcMain.handle('db:getFavorites', dbHandlers.getFavorites); ipcMain.handle('db:addFavorite', dbHandlers.addFavorite); ipcMain.handle('db:removeFavorite', dbHandlers.removeFavorite); diff --git a/src/content/image-handler.js b/src/content/image-handler.js index a81e819..974242a 100644 --- a/src/content/image-handler.js +++ b/src/content/image-handler.js @@ -1,115 +1,123 @@ -import { createAddFavoriteButton, createRemoveFavoriteButton } from '../favorites/favorites-handler.js'; +export function createImageCard(id, tags, imageUrl, thumbnailUrl, type, options = {}) { + const { + showMessage, + showTagModal, + favoriteIds = new Set() + } = options; + + const card = document.createElement('div'); + card.className = 'image-entry'; + card.dataset.id = id; + card.dataset.type = type; + card.title = tags.join(', '); + + const img = document.createElement('img'); + img.src = thumbnailUrl || imageUrl; + img.loading = 'lazy'; + img.alt = tags.join(' '); + img.onload = () => img.classList.add('loaded'); + + card.appendChild(img); + + if (type === 'book') { + const readOverlay = document.createElement('div'); + readOverlay.className = 'book-read-overlay'; + readOverlay.innerHTML = ` + + Click To Read + `; + card.appendChild(readOverlay); + + return card; + } + + const buttonsOverlay = document.createElement('div'); + buttonsOverlay.className = 'image-buttons'; + + const favBtn = document.createElement('button'); + favBtn.className = 'heart-button'; + favBtn.dataset.id = id; + + const isFavorited = favoriteIds.has(String(id)); + updateHeartIcon(favBtn, isFavorited); + + favBtn.onclick = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + const currentlyFavorited = favoriteIds.has(String(id)); + + if (currentlyFavorited) { + const success = await window.api.removeFavorite(id); + if (success) { + favoriteIds.delete(String(id)); + updateHeartIcon(favBtn, false); + showMessage('Removed from favorites', 'success'); + if (window.location.pathname.includes('favorites.html')) { + card.remove(); + if (options.applyLayoutToGallery && options.favoritesGallery) { + options.applyLayoutToGallery(options.favoritesGallery, options.currentLayout); + } + } + } else { + showMessage('Failed to remove favorite', 'error'); + } + } else { + const favoriteData = { + id: String(id), + image_url: imageUrl, + thumbnail_url: thumbnailUrl, + tags: tags.join(','), + title: card.title || 'Unknown' + }; + const success = await window.api.addFavorite(favoriteData); + if (success) { + favoriteIds.add(String(id)); + updateHeartIcon(favBtn, true); + showMessage('Added to favorites', 'success'); + } else { + showMessage('Failed to save favorite', 'error'); + } + } + }; + + const tagBtn = document.createElement('button'); + tagBtn.innerHTML = ``; + tagBtn.title = "View Tags"; + tagBtn.onclick = (e) => { + e.stopPropagation(); + showTagModal(tags); + }; + + buttonsOverlay.appendChild(tagBtn); + buttonsOverlay.appendChild(favBtn); + card.appendChild(buttonsOverlay); + + return card; +} + +function updateHeartIcon(btn, isFavorited) { + if (isFavorited) { + btn.innerHTML = ``; + btn.title = "Remove from Favorites"; + } else { + btn.innerHTML = ``; + btn.title = "Add to Favorites"; + } +} export function populateTagModal(container, tags) { - container.innerHTML = ''; - - if (!tags || tags.length === 0) { - container.innerHTML = - '
No tags available for this image.
'; - 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); + container.innerHTML = ''; + if (!tags || tags.length === 0) { + container.innerHTML = 'No tags available.
'; + return; } - }); - container.appendChild(fragment); -} - -function createInfoButton(safeTags, showTagModalCallback) { - 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(); - showTagModalCallback(safeTags); - }; - return button; -} - -export function createImageCard(id, tags, imageUrl, thumbnailUrl, type, context) { - const { - currentLayout, - showMessage, - showTagModal, - applyLayoutToGallery, - favoritesGallery - } = context; - - 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, showTagModal)); - - if (type === 'browse') { - buttonContainer.appendChild( - createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl, showMessage) - ); - } else { - buttonContainer.appendChild( - createRemoveFavoriteButton(id, favoritesGallery, showMessage, applyLayoutToGallery, currentLayout) - ); - } - imageContainer.appendChild(buttonContainer); - - return entry; + tags.forEach(tag => { + const span = document.createElement('span'); + span.textContent = tag; + container.appendChild(span); + }); } \ No newline at end of file diff --git a/src/extensions/load-extensions.js b/src/extensions/load-extensions.js index 717c52a..fb131a6 100644 --- a/src/extensions/load-extensions.js +++ b/src/extensions/load-extensions.js @@ -1,27 +1,22 @@ -export async function populateSources(sourceList) { - console.log('Requesting sources from main process...'); +export async function populateSources(sourceList, targetType = 'image-board') { + console.log(`Requesting sources for type: ${targetType}...`); const sources = await window.api.getSources(); sourceList.innerHTML = ''; let initialSource = ''; - console.log("Raw sources received from backend:", sources); + console.log("Raw sources received:", sources); if (sources && sources.length > 0) { sources.forEach((source) => { - if (source.name.toLowerCase().includes('tenor')) { - if (source.type !== 'image-board') { - console.error(`CRITICAL: Tenor extension found, but type is "${source.type}". It will be hidden.`); - } else { - console.log("SUCCESS: Tenor extension passed type check."); - } - } - - if (source.type !== 'image-board') { + + if (targetType !== 'all' && source.type !== targetType) { return; } const button = document.createElement('button'); + button.className = 'source-button hover:bg-gray-700 hover:text-white transition-all duration-200'; + button.dataset.source = source.name; button.title = source.name; @@ -81,7 +76,7 @@ export async function populateSources(sourceList) { firstButton.classList.add('active'); initialSource = firstButton.dataset.source; } else { - console.warn("All sources were filtered out. Check 'type' property in your extensions."); + console.warn(`No sources found for type: ${targetType}`); } setupCarousel(sourceList); diff --git a/src/ipc/api-handlers.js b/src/ipc/api-handlers.js index 3937a6d..a27c601 100644 --- a/src/ipc/api-handlers.js +++ b/src/ipc/api-handlers.js @@ -1,6 +1,7 @@ const fs = require('fs'); const fetchPath = require.resolve('node-fetch'); const cheerioPath = require.resolve('cheerio'); +const fetch = require(fetchPath); function peekProperty(filePath, propertyName) { try { @@ -17,7 +18,6 @@ module.exports = function (availableScrapers, headlessBrowser) { Object.keys(availableScrapers).forEach(name => { const scraper = availableScrapers[name]; - if (!scraper.url) { if (scraper.instance && scraper.instance.baseUrl) { scraper.url = scraper.instance.baseUrl; @@ -25,78 +25,128 @@ module.exports = function (availableScrapers, headlessBrowser) { scraper.url = peekProperty(scraper.path, 'baseUrl'); } } - if (!scraper.type) { if (scraper.instance && scraper.instance.type) { scraper.type = scraper.instance.type; } else { const typeFromFile = peekProperty(scraper.path, 'type'); - if (typeFromFile) { - console.log(`[API] Recovered type for ${name} via static analysis: ${typeFromFile}`); - scraper.type = typeFromFile; - } + if (typeFromFile) scraper.type = typeFromFile; } } }); - return { - getSources: () => { - console.log("[API] Handling getSources request..."); - - const results = Object.keys(availableScrapers).map((name) => { - const scraper = availableScrapers[name]; - - const typeToReturn = scraper.type || null; - - console.log(`[API] Processing ${name}: Type found = "${typeToReturn}"`); - - return { - name: name, - url: scraper.url || name, - type: typeToReturn - }; - }); - - return results; - }, - - search: async (event, source, query, page) => { + const getScraperInstance = (source) => { const scraperData = availableScrapers[source]; - - if (!scraperData) { - return { success: false, error: `Source ${source} not found.` }; - } + if (!scraperData) throw new Error(`Source ${source} not found.`); if (!scraperData.instance) { - console.log(`[LazyLoad] Initializing scraper: ${source}...`); - try { + console.log(`[LazyLoad] Initializing scraper: ${source}...`); const scraperModule = require(scraperData.path); const className = Object.keys(scraperModule)[0]; const ScraperClass = scraperModule[className]; - - if (!ScraperClass || typeof ScraperClass !== 'function') { - throw new Error(`File ${scraperData.path} does not export a valid class.`); - } - const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser); scraperData.instance = instance; if (instance.type) scraperData.type = instance.type; if (instance.baseUrl) scraperData.url = instance.baseUrl; - - } catch (err) { - console.error(`Failed to lazy load ${source}:`, err); - return { success: false, error: `Failed to load extension: ${err.message}` }; - } } + return scraperData.instance; + }; + return { + getSources: () => { + return Object.keys(availableScrapers).map((name) => { + const scraper = availableScrapers[name]; + return { + name: name, + url: scraper.url || name, + type: scraper.type || (scraper.instance ? scraper.instance.type : null) + }; + }); + }, + + search: async (event, source, query, page) => { try { - const results = await scraperData.instance.fetchSearchResult(query, page); + const instance = getScraperInstance(source); + const results = await instance.fetchSearchResult(query, page); return { success: true, data: results }; } catch (err) { console.error(`Error during search in ${source}:`, err); return { success: false, error: err.message }; } + }, + + getChapters: async (event, source, mangaId) => { + try { + const instance = getScraperInstance(source); + if (!instance.findChapters) throw new Error("Extension does not support chapters."); + + const result = await instance.findChapters(mangaId); + + if (Array.isArray(result)) { + return { success: true, data: result }; + } else if (result && result.chapters) { + return { success: true, data: result.chapters, extra: { cover: result.cover } }; + } + + return { success: true, data: [] }; + + } catch (err) { + console.error(`Error fetching chapters from ${source}:`, err); + return { success: false, error: err.message }; + } + }, + + getPages: async (event, source, chapterId) => { + try { + const instance = getScraperInstance(source); + if (!instance.findChapterPages) throw new Error("Extension does not support reading pages."); + const pages = await instance.findChapterPages(chapterId); + return { success: true, data: pages }; + } catch (err) { + console.error(`Error fetching pages from ${source}:`, err); + return { success: false, error: err.message }; + } + }, + + getMetadata: async (event, title) => { + let cleanTitle = title.replace(/(\[.*?\]|\(.*?\))/g, '').trim().replace(/\s+/g, ' '); + console.log(`[AniList] Searching for: "${cleanTitle}"`); + + const query = ` + query ($search: String, $type: MediaType) { + Media (search: $search, type: $type, sort: SEARCH_MATCH) { + id + title { romaji english native } + description(asHtml: false) + averageScore + genres + coverImage { extraLarge large } + characters(page: 1, perPage: 10, sort: ROLE) { + edges { + role + node { id name { full } image { medium } } + } + } + } + } + `; + + try { + const response = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ query, variables: { search: cleanTitle, type: 'MANGA' } }) + }); + + const json = await response.json(); + if (json.errors || !json.data || !json.data.Media) { + return { success: false, error: "No media found" }; + } + return { success: true, data: json.data.Media }; + } catch (err) { + return { success: false, error: err.message }; + } } }; }; \ No newline at end of file diff --git a/src/modules/search-handler.js b/src/modules/search-handler.js index 103d327..6724132 100644 --- a/src/modules/search-handler.js +++ b/src/modules/search-handler.js @@ -27,7 +27,9 @@ export async function performSearch( contentGallery.innerHTML = ''; updateHeader(); - searchModal.classList.add('hidden'); + if (searchModal) { + searchModal.classList.add('hidden'); + } await loadMoreResults(currentSource, 1, currentLayout, domRefs, callbacks); } @@ -42,9 +44,7 @@ export async function loadMoreResults( const { loadingSpinner, infiniteLoadingSpinner, contentGallery } = domRefs; const { applyLayoutToGallery, createImageCard } = callbacks; - if (isLoading || !hasNextPage) { - return; - } + if (isLoading || !hasNextPage) return; isLoading = true; @@ -64,16 +64,11 @@ export async function loadMoreResults( if (loadingSpinner) loadingSpinner.classList.add('hidden'); if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden'); - if ( - !result.success || - !result.data.results || - result.data.results.length === 0 - ) { + if (!result.success || !result.data.results || result.data.results.length === 0) { hasNextPage = false; if (page === 1) { applyLayoutToGallery(contentGallery, currentLayout); - contentGallery.innerHTML = - 'No results found. Please try another search term.
'; + contentGallery.innerHTML = 'No results found.
'; } isLoading = false; return; @@ -85,8 +80,7 @@ export async function loadMoreResults( if (page === 1) { hasNextPage = false; applyLayoutToGallery(contentGallery, currentLayout); - contentGallery.innerHTML = - 'Found results, but none had valid images.
'; + contentGallery.innerHTML = 'No valid images found.
'; } isLoading = false; return; @@ -96,21 +90,23 @@ export async function loadMoreResults( validResults.forEach((item) => { const thumbnailUrl = item.image; const displayUrl = item.sampleImageUrl || item.fullImageUrl || thumbnailUrl; - + const title = item.title || ''; + const card = createImageCard( item.id.toString(), - item.tags, + item.tags || [], displayUrl, thumbnailUrl, - 'browse' + item.type || 'browse' ); + + if (title) card.dataset.title = title; + fragment.appendChild(card); }); contentGallery.appendChild(fragment); - applyLayoutToGallery(contentGallery, currentLayout); - hasNextPage = result.data.hasNextPage; } catch (error) { diff --git a/src/preload.js b/src/preload.js index 227646d..8b356b4 100644 --- a/src/preload.js +++ b/src/preload.js @@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld('api', { addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav), removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id), + getChapters: (source, mangaId) => ipcRenderer.invoke('api:getChapters', source, mangaId), + getPages: (source, chapterId) => ipcRenderer.invoke('api:getPages', source, chapterId), + search: (source, query, page) => ipcRenderer.invoke('api:search', source, query, page), toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'), diff --git a/src/renderer.js b/src/renderer.js index a1d03c2..4650584 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -13,197 +13,286 @@ document.addEventListener('DOMContentLoaded', async () => { let currentSource = ''; let currentPage = 1; let isFetching = false; + const favoriteIds = new Set(); - function showMessage(message, type = 'success') { - if (domRefs.messageBar) { - uiShowMessage(domRefs.messageBar, message, type); - } + try { + if (window.api && window.api.getFavorites) { + const favs = await window.api.getFavorites(); + favs.forEach(f => favoriteIds.add(String(f.id))); + } + } catch (e) { console.error(e); } + + function showMessage(msg, type = 'success') { if (domRefs.messageBar) uiShowMessage(domRefs.messageBar, msg, type); } + function showTagModal(tags) { if (domRefs.tagInfoContent) { populateTagModal(domRefs.tagInfoContent, tags); domRefs.tagInfoModal.classList.remove('hidden'); } } + function localCreateImageCard(id, tags, img, thumb, type) { + return createImageCard(id, tags, img, thumb, type, { currentLayout, showMessage, showTagModal, applyLayoutToGallery, favoritesGallery: document.getElementById('favorites-gallery'), favoriteIds }); } + function updateHeader() { if (domRefs.headerContext) domRefs.headerContext.classList.add('hidden'); } - function showTagModal(tags) { - if (domRefs.tagInfoContent && domRefs.tagInfoModal) { - populateTagModal(domRefs.tagInfoContent, tags); - domRefs.tagInfoModal.classList.remove('hidden'); - } - } + const callbacks = { showMessage, applyLayoutToGallery, updateHeader, createImageCard: localCreateImageCard }; - function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) { - return createImageCard(id, tags, imageUrl, thumbnailUrl, type, { - currentLayout, - showMessage, - showTagModal, - applyLayoutToGallery, - favoritesGallery: document.getElementById('favorites-gallery') - }); - } + let currentChapters = []; + let currentChapterPage = 1; + const CHAPTERS_PER_PAGE = 10; - function updateHeader() { - if (!domRefs.headerContext) return; - domRefs.headerContext.classList.add('hidden'); - } + function renderChapterPage() { + const listContainer = document.getElementById('chapter-list-container'); + if (!listContainer) return; + listContainer.innerHTML = ''; - const callbacks = { - showMessage, - applyLayoutToGallery, - updateHeader, - createImageCard: localCreateImageCard - }; + const start = (currentChapterPage - 1) * CHAPTERS_PER_PAGE; + const end = start + CHAPTERS_PER_PAGE; + const slice = currentChapters.slice(start, end); - if (domRefs.searchModal) { - setupGlobalKeybinds(domRefs.searchModal); - } + if (slice.length === 0) { + listContainer.innerHTML = 'Loading chapters...
Loading content...
No content found.
'; + return; + } + + const isTextMode = response.data[0].type === 'text'; + + if (isTextMode) { + const pageData = response.data[0]; + const textDiv = document.createElement('div'); + textDiv.className = 'reader-text-content'; + textDiv.innerHTML = pageData.content; + readerContent.appendChild(textDiv); + } else { + response.data.forEach(page => { + const img = document.createElement('img'); + img.className = 'reader-page-img'; + img.src = page.url; + img.loading = "lazy"; + readerContent.appendChild(img); + }); + } + + } catch (err) { + console.error(err); + showMessage('Failed to load content', 'error'); + } + } + + if (domRefs.searchModal) setupGlobalKeybinds(domRefs.searchModal); + if (domRefs.tagInfoCloseButton) domRefs.tagInfoCloseButton.onclick = () => domRefs.tagInfoModal.classList.add('hidden'); + if (domRefs.searchIconButton) { + domRefs.searchIconButton.onclick = () => { domRefs.searchModal.classList.remove('hidden'); domRefs.searchInput?.focus(); }; + domRefs.searchCloseButton.onclick = () => domRefs.searchModal.classList.add('hidden'); } if (domRefs.sourceList) { - if (domRefs.contentGallery) { - applyLayoutToGallery(domRefs.contentGallery, currentLayout); - } + if (domRefs.contentGallery) applyLayoutToGallery(domRefs.contentGallery, currentLayout); - let initialSource = ''; - if (window.api && window.api.getSources) { - initialSource = await populateSources(domRefs.sourceList); - } else { - initialSource = await populateSources(domRefs.sourceList); - } - + const isBooksPage = window.location.pathname.includes('books.html'); + const contentType = isBooksPage ? 'book-board' : 'image-board'; + + let initialSource = await populateSources(domRefs.sourceList, contentType); currentSource = initialSource; updateHeader(); domRefs.sourceList.addEventListener('click', (e) => { const button = e.target.closest('.source-button'); if (button) { - domRefs.sourceList - .querySelectorAll('.source-button') - .forEach((btn) => btn.classList.remove('active')); + domRefs.sourceList.querySelectorAll('.source-button').forEach(b => b.classList.remove('active')); button.classList.add('active'); - currentSource = button.dataset.source; updateHeader(); - currentPage = 1; - - if (domRefs.searchInput && domRefs.searchInput.value.trim()) { - performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); - } else if (domRefs.searchInput) { - performSearch(currentSource, { value: "" }, currentLayout, domRefs, callbacks); - } + if (domRefs.searchInput?.value.trim()) performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); + else if (domRefs.searchInput) performSearch(currentSource, { value: "" }, currentLayout, domRefs, callbacks); } }); + if (domRefs.contentGallery) { + domRefs.contentGallery.addEventListener('click', (e) => { + const card = e.target.closest('.image-entry'); + if (card && isBooksPage) { + if (e.target.closest('button')) return; + e.preventDefault(); e.stopPropagation(); + + const bookId = card.dataset.id; + const img = card.querySelector('img'); + const title = card.dataset.title || "Unknown"; + + if (bookId) openBookDetails(bookId, img ? img.src : '', title, []); + } + }); + } + const scrollContainer = document.querySelector('.content-view'); if (scrollContainer) { scrollContainer.addEventListener('scroll', async () => { - if ( - scrollContainer.scrollTop + scrollContainer.clientHeight >= - scrollContainer.scrollHeight - 600 - ) { + if (scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 600) { if (isFetching) return; isFetching = true; - currentPage++; - - if (domRefs.infiniteLoadingSpinner) { - domRefs.infiniteLoadingSpinner.classList.remove('hidden'); - } - - try { - await loadMoreResults(currentSource, currentPage, currentLayout, domRefs, callbacks); - } catch (error) { - console.error("Failed to load more results:", error); - currentPage--; - } finally { - isFetching = false; - if (domRefs.infiniteLoadingSpinner) { - domRefs.infiniteLoadingSpinner.classList.add('hidden'); - } - } + if (domRefs.infiniteLoadingSpinner) domRefs.infiniteLoadingSpinner.classList.remove('hidden'); + try { await loadMoreResults(currentSource, currentPage, currentLayout, domRefs, callbacks); } + catch (error) { currentPage--; } + finally { isFetching = false; if (domRefs.infiniteLoadingSpinner) domRefs.infiniteLoadingSpinner.classList.add('hidden'); } } }); } - if (domRefs.searchButton && domRefs.searchInput) { - domRefs.searchButton.addEventListener('click', () => { - currentPage = 1; - performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); - }); + if (domRefs.searchButton) { + domRefs.searchButton.onclick = () => { currentPage = 1; performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); }; } } if (document.getElementById('favorites-gallery')) { const favGallery = document.getElementById('favorites-gallery'); - - const fetchFavorites = async () => { - try { - if (window.api && window.api.getFavorites) { - return await window.api.getFavorites(); - } else { - console.error("window.api.getFavorites is missing."); - return []; - } - } catch (err) { - console.error("Error fetching favorites via IPC:", err); - return []; - } - }; - - const rawFavorites = await fetchFavorites(); - + const rawFavorites = await window.api.getFavorites(); favGallery.innerHTML = ''; - - if (!rawFavorites || rawFavorites.length === 0) { - const emptyState = document.createElement('div'); - emptyState.className = 'loading-state'; - emptyState.style.gridColumn = '1 / -1'; - emptyState.innerHTML = 'No favorites found.
'; - favGallery.appendChild(emptyState); - } else { + if (!rawFavorites || rawFavorites.length === 0) favGallery.innerHTML = 'No favorites found.
Select a book source above to load content
+Fetching books...
+Loading more...
+Select a source above to load content
Fetching images...
Loading more...