Updated headless browser to support dynamic sites
Removed tabs and moved over to pages Updated the rendering system Fixed multiple pages not loading on scroll and re-rending or not rendering anything or just page 1. Fixed the search bar not taking in spaces for each query Updated how extensions are made Updated how extensions are loaded
This commit is contained in:
@@ -4,23 +4,40 @@ export async function populateSources(sourceList) {
|
||||
sourceList.innerHTML = '';
|
||||
let initialSource = '';
|
||||
|
||||
console.log("Raw sources received from backend:", 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') {
|
||||
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;
|
||||
|
||||
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('.');
|
||||
const urlToParse = source.url || source.baseUrl || "";
|
||||
if (urlToParse) {
|
||||
const hostname = new URL(urlToParse).hostname;
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
|
||||
mainDomain = parts.slice(1).join('.');
|
||||
} else {
|
||||
mainDomain = hostname;
|
||||
}
|
||||
} else {
|
||||
mainDomain = hostname;
|
||||
mainDomain = source.name;
|
||||
}
|
||||
} catch (e) {
|
||||
mainDomain = source.name;
|
||||
@@ -63,12 +80,14 @@ export async function populateSources(sourceList) {
|
||||
const firstButton = sourceList.children[0];
|
||||
firstButton.classList.add('active');
|
||||
initialSource = firstButton.dataset.source;
|
||||
} else {
|
||||
console.warn("All sources were filtered out. Check 'type' property in your extensions.");
|
||||
}
|
||||
|
||||
setupCarousel(sourceList);
|
||||
|
||||
} else {
|
||||
console.warn('No sources loaded.');
|
||||
console.warn('No sources loaded from API.');
|
||||
}
|
||||
return initialSource;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ const fs = require('fs');
|
||||
const fetchPath = require.resolve('node-fetch');
|
||||
const cheerioPath = require.resolve('cheerio');
|
||||
|
||||
function peekBaseUrl(filePath) {
|
||||
function peekProperty(filePath, propertyName) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const match = content.match(/baseUrl\s*=\s*["']([^"']+)["']/);
|
||||
const regex = new RegExp(`(?:this\\.|^|\\s)${propertyName}\\s*=\\s*["']([^"']+)["']`);
|
||||
const match = content.match(regex);
|
||||
return match ? match[1] : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -16,23 +17,47 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
||||
|
||||
Object.keys(availableScrapers).forEach(name => {
|
||||
const scraper = availableScrapers[name];
|
||||
|
||||
if (!scraper.url) {
|
||||
const url = peekBaseUrl(scraper.path);
|
||||
if (url) {
|
||||
scraper.url = url;
|
||||
if (scraper.instance && scraper.instance.baseUrl) {
|
||||
scraper.url = scraper.instance.baseUrl;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
getSources: () => {
|
||||
return Object.keys(availableScrapers).map((name) => {
|
||||
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
|
||||
url: scraper.url || name,
|
||||
type: typeToReturn
|
||||
};
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
search: async (event, source, query, page) => {
|
||||
@@ -46,7 +71,6 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
||||
console.log(`[LazyLoad] Initializing scraper: ${source}...`);
|
||||
try {
|
||||
const scraperModule = require(scraperData.path);
|
||||
|
||||
const className = Object.keys(scraperModule)[0];
|
||||
const ScraperClass = scraperModule[className];
|
||||
|
||||
@@ -55,12 +79,10 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
||||
}
|
||||
|
||||
const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser);
|
||||
|
||||
scraperData.instance = instance;
|
||||
|
||||
if (instance.baseUrl) {
|
||||
scraperData.url = instance.baseUrl;
|
||||
}
|
||||
if (instance.type) scraperData.type = instance.type;
|
||||
if (instance.baseUrl) scraperData.url = instance.baseUrl;
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Failed to lazy load ${source}:`, err);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
let currentPage = 1;
|
||||
let hasNextPage = true;
|
||||
let isLoading = false;
|
||||
let currentQuery = '';
|
||||
@@ -18,10 +17,9 @@ export async function performSearch(
|
||||
return;
|
||||
}
|
||||
|
||||
currentPage = 1;
|
||||
hasNextPage = true;
|
||||
isLoading = false;
|
||||
currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' ');
|
||||
currentQuery = searchInput.value ? searchInput.value.trim().replace(/[, ]+/g, ' ') : '';
|
||||
|
||||
if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
|
||||
|
||||
@@ -31,11 +29,12 @@ export async function performSearch(
|
||||
|
||||
searchModal.classList.add('hidden');
|
||||
|
||||
await loadMoreResults(currentSource, currentLayout, domRefs, callbacks);
|
||||
await loadMoreResults(currentSource, 1, currentLayout, domRefs, callbacks);
|
||||
}
|
||||
|
||||
export async function loadMoreResults(
|
||||
currentSource,
|
||||
page,
|
||||
currentLayout,
|
||||
domRefs,
|
||||
callbacks
|
||||
@@ -49,67 +48,76 @@ export async function loadMoreResults(
|
||||
|
||||
isLoading = true;
|
||||
|
||||
if (currentPage === 1) {
|
||||
loadingSpinner.classList.remove('hidden');
|
||||
if (page === 1) {
|
||||
if(loadingSpinner) loadingSpinner.classList.remove('hidden');
|
||||
} else {
|
||||
infiniteLoadingSpinner.classList.remove('hidden');
|
||||
if(infiniteLoadingSpinner) infiniteLoadingSpinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const result = await window.api.search(
|
||||
currentSource,
|
||||
currentQuery,
|
||||
currentPage
|
||||
);
|
||||
try {
|
||||
const result = await window.api.search(
|
||||
currentSource,
|
||||
currentQuery,
|
||||
page
|
||||
);
|
||||
|
||||
loadingSpinner.classList.add('hidden');
|
||||
infiniteLoadingSpinner.classList.add('hidden');
|
||||
if (loadingSpinner) loadingSpinner.classList.add('hidden');
|
||||
if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden');
|
||||
|
||||
if (
|
||||
!result.success ||
|
||||
!result.data.results ||
|
||||
result.data.results.length === 0
|
||||
) {
|
||||
hasNextPage = false;
|
||||
if (currentPage === 1) {
|
||||
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>';
|
||||
}
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const validResults = result.data.results.filter((item) => item.image);
|
||||
|
||||
if (validResults.length === 0) {
|
||||
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>';
|
||||
}
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
hasNextPage = result.data.hasNextPage;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Search/Load Error:", error);
|
||||
if (loadingSpinner) loadingSpinner.classList.add('hidden');
|
||||
if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
|
||||
removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id),
|
||||
|
||||
search: (source, query) => ipcRenderer.invoke('api:search', source, query),
|
||||
search: (source, query, page) => ipcRenderer.invoke('api:search', source, query, page),
|
||||
toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'),
|
||||
|
||||
getSources: () => ipcRenderer.invoke('api:getSources'),
|
||||
|
||||
257
src/renderer.js
257
src/renderer.js
@@ -1,27 +1,30 @@
|
||||
import { populateSources } from './extensions/load-extensions.js';
|
||||
import { setupGlobalKeybinds } from './utils/keybinds.js';
|
||||
import { getDomElements } from './utils/dom-loader.js';
|
||||
import { getDomElements } from './utils/dom-loader.js';
|
||||
import { performSearch, loadMoreResults } from './modules/search-handler.js';
|
||||
import { createImageCard, populateTagModal } from './content/image-handler.js';
|
||||
import { showMessage as uiShowMessage } from './modules/ui-utils.js';
|
||||
import { showPage as navShowPage } from './modules/navigation-handler.js';
|
||||
import { applyLayoutToGallery, loadSavedLayout, saveLayout } from './modules/layout-manager.js';
|
||||
import { applyLayoutToGallery } from './modules/layout-manager.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const domRefs = getDomElements();
|
||||
|
||||
const currentLayout = 'grid';
|
||||
let currentSource = '';
|
||||
let currentLayout = loadSavedLayout();
|
||||
|
||||
setupGlobalKeybinds(domRefs.searchModal);
|
||||
|
||||
let currentPage = 1;
|
||||
let isFetching = false;
|
||||
|
||||
function showMessage(message, type = 'success') {
|
||||
uiShowMessage(domRefs.messageBar, message, type);
|
||||
if (domRefs.messageBar) {
|
||||
uiShowMessage(domRefs.messageBar, message, type);
|
||||
}
|
||||
}
|
||||
|
||||
function showTagModal(tags) {
|
||||
populateTagModal(domRefs.tagInfoContent, tags);
|
||||
domRefs.tagInfoModal.classList.remove('hidden');
|
||||
if (domRefs.tagInfoContent && domRefs.tagInfoModal) {
|
||||
populateTagModal(domRefs.tagInfoContent, tags);
|
||||
domRefs.tagInfoModal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) {
|
||||
@@ -30,16 +33,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
showMessage,
|
||||
showTagModal,
|
||||
applyLayoutToGallery,
|
||||
favoritesGallery: domRefs.favoritesGallery
|
||||
favoritesGallery: document.getElementById('favorites-gallery')
|
||||
});
|
||||
}
|
||||
|
||||
function updateHeader() {
|
||||
if (currentSource) {
|
||||
domRefs.headerContext.textContent = `Source: ${currentSource}`;
|
||||
} else {
|
||||
domRefs.headerContext.textContent = 'No source selected';
|
||||
}
|
||||
if (!domRefs.headerContext) return;
|
||||
domRefs.headerContext.classList.add('hidden');
|
||||
}
|
||||
|
||||
const callbacks = {
|
||||
@@ -49,84 +49,161 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
createImageCard: localCreateImageCard
|
||||
};
|
||||
|
||||
function handleNavigation(pageId) {
|
||||
navShowPage(pageId, domRefs, callbacks, { currentLayout });
|
||||
if (domRefs.searchModal) {
|
||||
setupGlobalKeybinds(domRefs.searchModal);
|
||||
}
|
||||
|
||||
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'));
|
||||
button.classList.add('active');
|
||||
|
||||
currentSource = button.dataset.source;
|
||||
console.log('Source changed to:', currentSource);
|
||||
updateHeader();
|
||||
|
||||
if (domRefs.searchInput.value.trim()) {
|
||||
performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
domRefs.browseButton.addEventListener('click', () => handleNavigation('browse-page'));
|
||||
domRefs.favoritesButton.addEventListener('click', () => handleNavigation('favorites-page'));
|
||||
domRefs.settingsButton.addEventListener('click', () => handleNavigation('settings-page'));
|
||||
|
||||
domRefs.searchIconButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.remove('hidden');
|
||||
domRefs.searchInput.focus();
|
||||
domRefs.searchInput.select();
|
||||
});
|
||||
domRefs.searchCloseButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.add('hidden');
|
||||
});
|
||||
domRefs.searchButton.addEventListener('click', () => {
|
||||
performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks);
|
||||
});
|
||||
|
||||
domRefs.tagInfoCloseButton.addEventListener('click', () => {
|
||||
domRefs.tagInfoModal.classList.add('hidden');
|
||||
});
|
||||
domRefs.tagInfoModal.addEventListener('click', (e) => {
|
||||
if (e.target === domRefs.tagInfoModal) {
|
||||
domRefs.tagInfoModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
domRefs.browsePage.addEventListener('scroll', () => {
|
||||
if (
|
||||
domRefs.browsePage.scrollTop + domRefs.browsePage.clientHeight >=
|
||||
domRefs.browsePage.scrollHeight - 600
|
||||
) {
|
||||
loadMoreResults(currentSource, currentLayout, domRefs, callbacks);
|
||||
}
|
||||
});
|
||||
|
||||
domRefs.layoutRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const newLayout = e.target.value;
|
||||
saveLayout(newLayout);
|
||||
currentLayout = newLayout;
|
||||
|
||||
if (domRefs.browsePage.classList.contains('hidden')) {
|
||||
handleNavigation('favorites-page');
|
||||
} else {
|
||||
if (domRefs.searchInput.value.trim()) {
|
||||
performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks);
|
||||
} else {
|
||||
applyLayoutToGallery(domRefs.contentGallery, currentLayout);
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (domRefs.searchIconButton && domRefs.searchModal) {
|
||||
domRefs.searchIconButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.remove('hidden');
|
||||
if(domRefs.searchInput) {
|
||||
domRefs.searchInput.focus();
|
||||
domRefs.searchInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
domRefs.searchCloseButton.addEventListener('click', () => {
|
||||
domRefs.searchModal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (domRefs.sourceList) {
|
||||
if (domRefs.contentGallery) {
|
||||
applyLayoutToGallery(domRefs.contentGallery, currentLayout);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const initialSource = await populateSources(domRefs.sourceList);
|
||||
currentSource = initialSource;
|
||||
let initialSource = '';
|
||||
if (window.api && window.api.getSources) {
|
||||
initialSource = await populateSources(domRefs.sourceList);
|
||||
} else {
|
||||
initialSource = await populateSources(domRefs.sourceList);
|
||||
}
|
||||
|
||||
currentSource = initialSource;
|
||||
updateHeader();
|
||||
|
||||
updateHeader();
|
||||
handleNavigation('browse-page');
|
||||
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'));
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const scrollContainer = document.querySelector('.content-view');
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('scroll', async () => {
|
||||
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.searchButton && domRefs.searchInput) {
|
||||
domRefs.searchButton.addEventListener('click', () => {
|
||||
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();
|
||||
|
||||
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 {
|
||||
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'
|
||||
);
|
||||
favGallery.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
applyLayoutToGallery(favGallery, currentLayout);
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,14 @@ const { BrowserWindow } = require('electron');
|
||||
|
||||
class HeadlessBrowser {
|
||||
async scrape(url, evalFunc, options = {}) {
|
||||
const { waitSelector = null, timeout = 15000 } = options;
|
||||
const {
|
||||
waitSelector = null,
|
||||
timeout = 15000,
|
||||
args = [],
|
||||
scrollToBottom = false,
|
||||
renderWaitTime = 2000,
|
||||
loadImages = true
|
||||
} = options;
|
||||
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
@@ -12,7 +19,7 @@ class HeadlessBrowser {
|
||||
offscreen: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: false,
|
||||
images: false,
|
||||
images: loadImages,
|
||||
webgl: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
@@ -23,32 +30,37 @@ class HeadlessBrowser {
|
||||
win.webContents.setUserAgent(userAgent);
|
||||
|
||||
const session = win.webContents.session;
|
||||
const filter = { urls: ['*://*/*'] };
|
||||
|
||||
session.webRequest.onBeforeRequest(filter, (details, callback) => {
|
||||
session.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
const url = details.url.toLowerCase();
|
||||
|
||||
const blockExtensions = [
|
||||
'.css', '.woff', '.woff2', '.ttf', '.svg', '.eot',
|
||||
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem'
|
||||
'.woff', '.woff2', '.ttf', '.eot',
|
||||
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem'
|
||||
];
|
||||
|
||||
const isBlocked = blockExtensions.some(ext => url.includes(ext));
|
||||
|
||||
if (isBlocked) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
|
||||
if (blockExtensions.some(ext => url.includes(ext))) return callback({ cancel: true });
|
||||
return callback({ cancel: false });
|
||||
});
|
||||
|
||||
await win.loadURL(url, { userAgent });
|
||||
|
||||
if (waitSelector) {
|
||||
await this.waitForSelector(win, waitSelector, timeout);
|
||||
try {
|
||||
await this.waitForSelector(win, waitSelector, timeout);
|
||||
} catch (e) {
|
||||
console.warn(`[Headless] Timeout waiting for ${waitSelector}, proceeding anyway...`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await win.webContents.executeJavaScript(`(${evalFunc.toString()})()`);
|
||||
if (scrollToBottom) {
|
||||
await this.smoothScrollToBottom(win);
|
||||
}
|
||||
|
||||
if (renderWaitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, renderWaitTime));
|
||||
}
|
||||
|
||||
const result = await win.webContents.executeJavaScript(
|
||||
`(${evalFunc.toString()}).apply(null, ${JSON.stringify(args)})`
|
||||
);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -70,11 +82,12 @@ class HeadlessBrowser {
|
||||
}, ${timeout});
|
||||
|
||||
const check = () => {
|
||||
if (document.querySelector('${selector}')) {
|
||||
const el = document.querySelector('${selector}');
|
||||
if (el) {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
setTimeout(check, 200);
|
||||
}
|
||||
};
|
||||
check();
|
||||
@@ -82,6 +95,30 @@ class HeadlessBrowser {
|
||||
`;
|
||||
await win.webContents.executeJavaScript(script);
|
||||
}
|
||||
|
||||
async smoothScrollToBottom(win) {
|
||||
const script = `
|
||||
new Promise((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 400;
|
||||
const maxScrolls = 200;
|
||||
let currentScrolls = 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();
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
`;
|
||||
await win.webContents.executeJavaScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new HeadlessBrowser();
|
||||
Reference in New Issue
Block a user