Updated headless browser (Should be as fast as it was before)
Added in Book Boards! (NEW!) Updated rendering logic Updated search logic Updated extension loading logic Updated image handling logic
This commit is contained in:
@@ -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 = `
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
<span>Click To Read</span>
|
||||
`;
|
||||
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 = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>`;
|
||||
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 = `<svg viewBox="0 0 24 24" fill="#ef4444" stroke="#ef4444" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
|
||||
btn.title = "Remove from Favorites";
|
||||
} else {
|
||||
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
|
||||
btn.title = "Add to Favorites";
|
||||
}
|
||||
}
|
||||
|
||||
export function populateTagModal(container, tags) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-gray-400">No tags available for this image.</p>';
|
||||
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 = '<p style="color:var(--text-tertiary)">No tags available.</p>';
|
||||
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 = `<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();
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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 =
|
||||
'<p class="text-gray-400 text-center text-lg">No results found. Please try another search term.</p>';
|
||||
contentGallery.innerHTML = '<p class="text-gray-400 text-center text-lg">No results found.</p>';
|
||||
}
|
||||
isLoading = false;
|
||||
return;
|
||||
@@ -85,8 +80,7 @@ export async function loadMoreResults(
|
||||
if (page === 1) {
|
||||
hasNextPage = false;
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
contentGallery.innerHTML =
|
||||
'<p class="text-gray-400 text-center text-lg">Found results, but none had valid images.</p>';
|
||||
contentGallery.innerHTML = '<p class="text-gray-400 text-center text-lg">No valid images found.</p>';
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
371
src/renderer.js
371
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 = '<div style="padding:1.5rem; text-align:center; color:var(--text-tertiary)">No chapters available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (domRefs.tagInfoCloseButton && domRefs.tagInfoModal) {
|
||||
domRefs.tagInfoCloseButton.addEventListener('click', () => {
|
||||
domRefs.tagInfoModal.classList.add('hidden');
|
||||
});
|
||||
domRefs.tagInfoModal.addEventListener('click', (e) => {
|
||||
if (e.target === domRefs.tagInfoModal) {
|
||||
domRefs.tagInfoModal.classList.add('hidden');
|
||||
}
|
||||
slice.forEach(chapter => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'chapter-row';
|
||||
|
||||
let mainText = chapter.chapter && chapter.chapter !== '0' ? `Chapter ${chapter.chapter}` : 'Read';
|
||||
if(chapter.title && !chapter.title.includes(chapter.chapter)) {
|
||||
mainText = chapter.title;
|
||||
}
|
||||
|
||||
row.innerHTML = `<span class="chapter-main-text">${mainText}</span>`;
|
||||
row.onclick = () => openReader(chapter.id);
|
||||
listContainer.appendChild(row);
|
||||
});
|
||||
|
||||
const controls = document.getElementById('pagination-controls');
|
||||
if (controls) {
|
||||
controls.innerHTML = '';
|
||||
if (currentChapters.length > CHAPTERS_PER_PAGE) {
|
||||
const prev = document.createElement('button');
|
||||
prev.className = 'page-btn';
|
||||
prev.textContent = '← Prev';
|
||||
prev.disabled = currentChapterPage === 1;
|
||||
prev.onclick = () => { currentChapterPage--; renderChapterPage(); };
|
||||
|
||||
const next = document.createElement('button');
|
||||
next.className = 'page-btn';
|
||||
next.textContent = 'Next →';
|
||||
next.disabled = end >= currentChapters.length;
|
||||
next.onclick = () => { currentChapterPage++; renderChapterPage(); };
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.style.color = 'var(--text-secondary)';
|
||||
label.style.fontSize = '0.9rem';
|
||||
label.textContent = `Page ${currentChapterPage} of ${Math.ceil(currentChapters.length / CHAPTERS_PER_PAGE)}`;
|
||||
|
||||
controls.appendChild(prev);
|
||||
controls.appendChild(label);
|
||||
controls.appendChild(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (domRefs.searchIconButton && domRefs.searchModal) {
|
||||
domRefs.searchIconButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.remove('hidden');
|
||||
if(domRefs.searchInput) {
|
||||
domRefs.searchInput.focus();
|
||||
domRefs.searchInput.select();
|
||||
}
|
||||
});
|
||||
async function openBookDetails(id, imageUrl, title, tags) {
|
||||
const detailsView = document.getElementById('book-details-view');
|
||||
const browseView = document.getElementById('browse-page');
|
||||
if (!detailsView || !browseView) return;
|
||||
|
||||
browseView.classList.add('hidden');
|
||||
detailsView.classList.remove('hidden');
|
||||
|
||||
domRefs.searchCloseButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.add('hidden');
|
||||
});
|
||||
detailsView.innerHTML = `
|
||||
<div class="book-top-nav">
|
||||
<div class="back-btn-large" id="back-to-library">
|
||||
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
Back to Library
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-layout-grid">
|
||||
<div class="book-left-col">
|
||||
<img src="${imageUrl}" class="book-poster-large" id="book-details-poster" />
|
||||
<h1 class="book-title-sidebar">${title}</h1>
|
||||
</div>
|
||||
|
||||
<div class="book-chapters-column">
|
||||
<div class="chapter-table-container" id="chapter-list-container">
|
||||
<div class="loading-state" style="padding:2rem;"><p>Loading chapters...</p></div>
|
||||
</div>
|
||||
<div id="pagination-controls" class="pagination-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('back-to-library').onclick = () => {
|
||||
detailsView.classList.add('hidden');
|
||||
browseView.classList.remove('hidden');
|
||||
};
|
||||
|
||||
let highResCover = null;
|
||||
try {
|
||||
const aniRes = await window.api.getMetadata(title);
|
||||
if (aniRes.success && aniRes.data && aniRes.data.coverImage.extraLarge) {
|
||||
highResCover = aniRes.data.coverImage.extraLarge;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const response = await window.api.getChapters(currentSource, id);
|
||||
currentChapters = response.success ? response.data : [];
|
||||
currentChapterPage = 1;
|
||||
|
||||
if (!highResCover && response.extra && response.extra.cover) {
|
||||
highResCover = response.extra.cover;
|
||||
}
|
||||
|
||||
if (highResCover) {
|
||||
const posterEl = document.getElementById('book-details-poster');
|
||||
if (posterEl) posterEl.src = highResCover;
|
||||
}
|
||||
|
||||
renderChapterPage();
|
||||
} catch (err) {
|
||||
const chContainer = document.getElementById('chapter-list-container');
|
||||
if(chContainer) chContainer.innerHTML = '<div style="padding:1.5rem; text-align:center; color:#ef4444">Failed to load chapters.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function openReader(chapterId) {
|
||||
const detailsView = document.getElementById('book-details-view');
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const readerContent = document.getElementById('reader-content');
|
||||
if (!detailsView || !readerView) return;
|
||||
|
||||
detailsView.classList.add('hidden');
|
||||
readerView.classList.remove('hidden');
|
||||
readerContent.innerHTML = '<div class="loading-state"><p style="color:white;">Loading content...</p></div>';
|
||||
|
||||
const existingBackBtn = readerView.querySelector('.reader-close-btn');
|
||||
if(existingBackBtn) existingBackBtn.remove();
|
||||
|
||||
const backBtn = document.createElement('div');
|
||||
backBtn.className = 'reader-close-btn';
|
||||
backBtn.innerHTML = '<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg> Close Reader';
|
||||
backBtn.onclick = () => {
|
||||
readerView.classList.add('hidden');
|
||||
detailsView.classList.remove('hidden');
|
||||
readerContent.innerHTML = '';
|
||||
};
|
||||
readerView.appendChild(backBtn);
|
||||
|
||||
try {
|
||||
const response = await window.api.getPages(currentSource, chapterId);
|
||||
readerContent.innerHTML = '';
|
||||
|
||||
if (!response.success || response.data.length === 0) {
|
||||
readerContent.innerHTML = '<p style="color:white;">No content found.</p>';
|
||||
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 = '<p>No favorites found.</p>';
|
||||
favGallery.appendChild(emptyState);
|
||||
} else {
|
||||
if (!rawFavorites || rawFavorites.length === 0) favGallery.innerHTML = '<div class="loading-state"><p>No favorites found.</p></div>';
|
||||
else {
|
||||
rawFavorites.forEach(row => {
|
||||
const id = row.id;
|
||||
const imageUrl = row.image_url;
|
||||
const thumbnailUrl = row.thumbnail_url;
|
||||
|
||||
let tags = [];
|
||||
if (typeof row.tags === 'string') {
|
||||
tags = row.tags.split(',').filter(t => t.trim() !== '');
|
||||
} else if (Array.isArray(row.tags)) {
|
||||
tags = row.tags;
|
||||
}
|
||||
|
||||
const card = localCreateImageCard(
|
||||
id,
|
||||
tags,
|
||||
imageUrl,
|
||||
thumbnailUrl,
|
||||
'image'
|
||||
);
|
||||
if (typeof row.tags === 'string') tags = row.tags.split(',').filter(t=>t);
|
||||
else if (Array.isArray(row.tags)) tags = row.tags;
|
||||
const card = localCreateImageCard(row.id, tags, row.image_url, row.thumbnail_url, 'image');
|
||||
card.dataset.title = row.title;
|
||||
favGallery.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
applyLayoutToGallery(favGallery, currentLayout);
|
||||
}
|
||||
});
|
||||
@@ -1,17 +1,37 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, session } = require('electron');
|
||||
|
||||
class HeadlessBrowser {
|
||||
async scrape(url, evalFunc, options = {}) {
|
||||
const {
|
||||
waitSelector = null,
|
||||
timeout = 15000,
|
||||
args = [],
|
||||
scrollToBottom = false,
|
||||
renderWaitTime = 2000,
|
||||
loadImages = true
|
||||
} = options;
|
||||
constructor() {
|
||||
this.win = null;
|
||||
this.currentConfig = null;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow({
|
||||
/**
|
||||
* Pre-loads the browser window on app startup.
|
||||
*/
|
||||
async init() {
|
||||
console.log('[Headless] Pre-warming browser instance...');
|
||||
await this.getWindow(true); // Default to loading images
|
||||
console.log('[Headless] Browser ready.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an existing window or creates a new one if config changes/window missing.
|
||||
*/
|
||||
async getWindow(loadImages) {
|
||||
// If window exists and config matches, reuse it (FAST PATH)
|
||||
if (this.win && !this.win.isDestroyed() && this.currentConfig === loadImages) {
|
||||
return this.win;
|
||||
}
|
||||
|
||||
// Otherwise, destroy old window and create new one (SLOW PATH)
|
||||
if (this.win && !this.win.isDestroyed()) {
|
||||
this.win.destroy();
|
||||
}
|
||||
|
||||
this.currentConfig = loadImages;
|
||||
|
||||
this.win = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@@ -22,36 +42,75 @@ class HeadlessBrowser {
|
||||
images: loadImages,
|
||||
webgl: false,
|
||||
backgroundThrottling: false,
|
||||
autoplayPolicy: 'no-user-gesture-required',
|
||||
disableHtmlFullscreenWindowResize: true
|
||||
},
|
||||
});
|
||||
|
||||
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36';
|
||||
this.win.webContents.setUserAgent(userAgent);
|
||||
|
||||
const ses = this.win.webContents.session;
|
||||
|
||||
ses.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
const url = details.url.toLowerCase();
|
||||
const type = details.resourceType;
|
||||
|
||||
if (
|
||||
type === 'font' ||
|
||||
type === 'stylesheet' ||
|
||||
type === 'media' ||
|
||||
type === 'websocket' ||
|
||||
type === 'manifest'
|
||||
) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
|
||||
const blockList = [
|
||||
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem',
|
||||
'analytics', 'tracker', 'pixel', 'quantserve', 'newrelic'
|
||||
];
|
||||
|
||||
if (blockList.some(keyword => url.includes(keyword))) return callback({ cancel: true });
|
||||
|
||||
if (!loadImages && (type === 'image' || url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/))) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
|
||||
return callback({ cancel: false });
|
||||
});
|
||||
|
||||
// Load a blank page to keep the process alive and ready
|
||||
await this.win.loadURL('about:blank');
|
||||
|
||||
return this.win;
|
||||
}
|
||||
|
||||
async scrape(url, evalFunc, options = {}) {
|
||||
const {
|
||||
waitSelector = null,
|
||||
timeout = 10000,
|
||||
args = [],
|
||||
scrollToBottom = false,
|
||||
renderWaitTime = 0,
|
||||
loadImages = true
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
||||
win.webContents.setUserAgent(userAgent);
|
||||
const win = await this.getWindow(loadImages);
|
||||
|
||||
const session = win.webContents.session;
|
||||
session.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
const url = details.url.toLowerCase();
|
||||
const blockExtensions = [
|
||||
'.woff', '.woff2', '.ttf', '.eot',
|
||||
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem'
|
||||
];
|
||||
if (blockExtensions.some(ext => url.includes(ext))) return callback({ cancel: true });
|
||||
return callback({ cancel: false });
|
||||
});
|
||||
|
||||
await win.loadURL(url, { userAgent });
|
||||
await win.loadURL(url);
|
||||
|
||||
if (waitSelector) {
|
||||
try {
|
||||
await this.waitForSelector(win, waitSelector, timeout);
|
||||
} catch (e) {
|
||||
console.warn(`[Headless] Timeout waiting for ${waitSelector}, proceeding anyway...`);
|
||||
console.warn(`[Headless] Timeout waiting for ${waitSelector}, proceeding...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollToBottom) {
|
||||
await this.smoothScrollToBottom(win);
|
||||
await this.turboScroll(win);
|
||||
}
|
||||
|
||||
if (renderWaitTime > 0) {
|
||||
@@ -66,28 +125,26 @@ class HeadlessBrowser {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Headless Scrape Error:', error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
if (!win.isDestroyed()) {
|
||||
win.destroy();
|
||||
// Force recreation next time if something crashed
|
||||
if (this.win) {
|
||||
try { this.win.destroy(); } catch(e){}
|
||||
this.win = null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForSelector(win, selector, timeout) {
|
||||
const script = `
|
||||
new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for selector: ${selector}'));
|
||||
}, ${timeout});
|
||||
|
||||
const start = Date.now();
|
||||
const check = () => {
|
||||
const el = document.querySelector('${selector}');
|
||||
if (el) {
|
||||
clearTimeout(timer);
|
||||
if (document.querySelector('${selector}')) {
|
||||
resolve(true);
|
||||
} else if (Date.now() - start > ${timeout}) {
|
||||
reject(new Error('Timeout'));
|
||||
} else {
|
||||
setTimeout(check, 200);
|
||||
requestAnimationFrame(check);
|
||||
}
|
||||
};
|
||||
check();
|
||||
@@ -96,23 +153,23 @@ class HeadlessBrowser {
|
||||
await win.webContents.executeJavaScript(script);
|
||||
}
|
||||
|
||||
async smoothScrollToBottom(win) {
|
||||
async turboScroll(win) {
|
||||
const script = `
|
||||
new Promise((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 400;
|
||||
const maxScrolls = 200;
|
||||
let currentScrolls = 0;
|
||||
|
||||
let lastHeight = 0;
|
||||
let sameHeightCount = 0;
|
||||
const timer = setInterval(() => {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
currentScrolls++;
|
||||
|
||||
if(totalHeight >= scrollHeight - window.innerHeight || currentScrolls >= maxScrolls){
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
window.scrollTo(0, scrollHeight);
|
||||
if (scrollHeight === lastHeight) {
|
||||
sameHeightCount++;
|
||||
if (sameHeightCount >= 5) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
} else {
|
||||
sameHeightCount = 0;
|
||||
lastHeight = scrollHeight;
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user