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:
1
main.js
1
main.js
@@ -62,6 +62,7 @@ function createWindow() {
|
|||||||
preload: path.join(__dirname, '/src/preload.js'),
|
preload: path.join(__dirname, '/src/preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
|
enableRemoteModule: true
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,41 @@ export async function populateSources(sourceList) {
|
|||||||
sourceList.innerHTML = '';
|
sourceList.innerHTML = '';
|
||||||
let initialSource = '';
|
let initialSource = '';
|
||||||
|
|
||||||
|
console.log("Raw sources received from backend:", sources);
|
||||||
|
|
||||||
if (sources && sources.length > 0) {
|
if (sources && sources.length > 0) {
|
||||||
sources.forEach((source) => {
|
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');
|
const button = document.createElement('button');
|
||||||
|
|
||||||
button.className = 'source-button hover:bg-gray-700 hover:text-white transition-all duration-200';
|
button.className = 'source-button hover:bg-gray-700 hover:text-white transition-all duration-200';
|
||||||
|
|
||||||
button.dataset.source = source.name;
|
button.dataset.source = source.name;
|
||||||
button.title = source.name;
|
button.title = source.name;
|
||||||
|
|
||||||
let mainDomain = source.url;
|
let mainDomain = source.url;
|
||||||
try {
|
try {
|
||||||
const hostname = new URL(source.url).hostname;
|
const urlToParse = source.url || source.baseUrl || "";
|
||||||
|
if (urlToParse) {
|
||||||
|
const hostname = new URL(urlToParse).hostname;
|
||||||
const parts = hostname.split('.');
|
const parts = hostname.split('.');
|
||||||
if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
|
if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
|
||||||
mainDomain = parts.slice(1).join('.');
|
mainDomain = parts.slice(1).join('.');
|
||||||
} else {
|
} else {
|
||||||
mainDomain = hostname;
|
mainDomain = hostname;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
mainDomain = source.name;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mainDomain = source.name;
|
mainDomain = source.name;
|
||||||
}
|
}
|
||||||
@@ -63,12 +80,14 @@ export async function populateSources(sourceList) {
|
|||||||
const firstButton = sourceList.children[0];
|
const firstButton = sourceList.children[0];
|
||||||
firstButton.classList.add('active');
|
firstButton.classList.add('active');
|
||||||
initialSource = firstButton.dataset.source;
|
initialSource = firstButton.dataset.source;
|
||||||
|
} else {
|
||||||
|
console.warn("All sources were filtered out. Check 'type' property in your extensions.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCarousel(sourceList);
|
setupCarousel(sourceList);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('No sources loaded.');
|
console.warn('No sources loaded from API.');
|
||||||
}
|
}
|
||||||
return initialSource;
|
return initialSource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ const fs = require('fs');
|
|||||||
const fetchPath = require.resolve('node-fetch');
|
const fetchPath = require.resolve('node-fetch');
|
||||||
const cheerioPath = require.resolve('cheerio');
|
const cheerioPath = require.resolve('cheerio');
|
||||||
|
|
||||||
function peekBaseUrl(filePath) {
|
function peekProperty(filePath, propertyName) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
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;
|
return match ? match[1] : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -16,23 +17,47 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
|||||||
|
|
||||||
Object.keys(availableScrapers).forEach(name => {
|
Object.keys(availableScrapers).forEach(name => {
|
||||||
const scraper = availableScrapers[name];
|
const scraper = availableScrapers[name];
|
||||||
|
|
||||||
if (!scraper.url) {
|
if (!scraper.url) {
|
||||||
const url = peekBaseUrl(scraper.path);
|
if (scraper.instance && scraper.instance.baseUrl) {
|
||||||
if (url) {
|
scraper.url = scraper.instance.baseUrl;
|
||||||
scraper.url = url;
|
} 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 {
|
return {
|
||||||
getSources: () => {
|
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 scraper = availableScrapers[name];
|
||||||
|
|
||||||
|
const typeToReturn = scraper.type || null;
|
||||||
|
|
||||||
|
console.log(`[API] Processing ${name}: Type found = "${typeToReturn}"`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
url: scraper.url || name
|
url: scraper.url || name,
|
||||||
|
type: typeToReturn
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
},
|
},
|
||||||
|
|
||||||
search: async (event, source, query, page) => {
|
search: async (event, source, query, page) => {
|
||||||
@@ -46,7 +71,6 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
|||||||
console.log(`[LazyLoad] Initializing scraper: ${source}...`);
|
console.log(`[LazyLoad] Initializing scraper: ${source}...`);
|
||||||
try {
|
try {
|
||||||
const scraperModule = require(scraperData.path);
|
const scraperModule = require(scraperData.path);
|
||||||
|
|
||||||
const className = Object.keys(scraperModule)[0];
|
const className = Object.keys(scraperModule)[0];
|
||||||
const ScraperClass = scraperModule[className];
|
const ScraperClass = scraperModule[className];
|
||||||
|
|
||||||
@@ -55,12 +79,10 @@ module.exports = function (availableScrapers, headlessBrowser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser);
|
const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser);
|
||||||
|
|
||||||
scraperData.instance = instance;
|
scraperData.instance = instance;
|
||||||
|
|
||||||
if (instance.baseUrl) {
|
if (instance.type) scraperData.type = instance.type;
|
||||||
scraperData.url = instance.baseUrl;
|
if (instance.baseUrl) scraperData.url = instance.baseUrl;
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to lazy load ${source}:`, err);
|
console.error(`Failed to lazy load ${source}:`, err);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
let currentPage = 1;
|
|
||||||
let hasNextPage = true;
|
let hasNextPage = true;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let currentQuery = '';
|
let currentQuery = '';
|
||||||
@@ -18,10 +17,9 @@ export async function performSearch(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPage = 1;
|
|
||||||
hasNextPage = true;
|
hasNextPage = true;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' ');
|
currentQuery = searchInput.value ? searchInput.value.trim().replace(/[, ]+/g, ' ') : '';
|
||||||
|
|
||||||
if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
|
if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
|
||||||
|
|
||||||
@@ -31,11 +29,12 @@ export async function performSearch(
|
|||||||
|
|
||||||
searchModal.classList.add('hidden');
|
searchModal.classList.add('hidden');
|
||||||
|
|
||||||
await loadMoreResults(currentSource, currentLayout, domRefs, callbacks);
|
await loadMoreResults(currentSource, 1, currentLayout, domRefs, callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMoreResults(
|
export async function loadMoreResults(
|
||||||
currentSource,
|
currentSource,
|
||||||
|
page,
|
||||||
currentLayout,
|
currentLayout,
|
||||||
domRefs,
|
domRefs,
|
||||||
callbacks
|
callbacks
|
||||||
@@ -49,20 +48,21 @@ export async function loadMoreResults(
|
|||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
if (currentPage === 1) {
|
if (page === 1) {
|
||||||
loadingSpinner.classList.remove('hidden');
|
if(loadingSpinner) loadingSpinner.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
infiniteLoadingSpinner.classList.remove('hidden');
|
if(infiniteLoadingSpinner) infiniteLoadingSpinner.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const result = await window.api.search(
|
const result = await window.api.search(
|
||||||
currentSource,
|
currentSource,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
currentPage
|
page
|
||||||
);
|
);
|
||||||
|
|
||||||
loadingSpinner.classList.add('hidden');
|
if (loadingSpinner) loadingSpinner.classList.add('hidden');
|
||||||
infiniteLoadingSpinner.classList.add('hidden');
|
if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden');
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!result.success ||
|
!result.success ||
|
||||||
@@ -70,7 +70,7 @@ export async function loadMoreResults(
|
|||||||
result.data.results.length === 0
|
result.data.results.length === 0
|
||||||
) {
|
) {
|
||||||
hasNextPage = false;
|
hasNextPage = false;
|
||||||
if (currentPage === 1) {
|
if (page === 1) {
|
||||||
applyLayoutToGallery(contentGallery, currentLayout);
|
applyLayoutToGallery(contentGallery, currentLayout);
|
||||||
contentGallery.innerHTML =
|
contentGallery.innerHTML =
|
||||||
'<p class="text-gray-400 text-center text-lg">No results found. Please try another search term.</p>';
|
'<p class="text-gray-400 text-center text-lg">No results found. Please try another search term.</p>';
|
||||||
@@ -82,8 +82,8 @@ export async function loadMoreResults(
|
|||||||
const validResults = result.data.results.filter((item) => item.image);
|
const validResults = result.data.results.filter((item) => item.image);
|
||||||
|
|
||||||
if (validResults.length === 0) {
|
if (validResults.length === 0) {
|
||||||
|
if (page === 1) {
|
||||||
hasNextPage = false;
|
hasNextPage = false;
|
||||||
if (currentPage === 1) {
|
|
||||||
applyLayoutToGallery(contentGallery, currentLayout);
|
applyLayoutToGallery(contentGallery, currentLayout);
|
||||||
contentGallery.innerHTML =
|
contentGallery.innerHTML =
|
||||||
'<p class="text-gray-400 text-center text-lg">Found results, but none had valid images.</p>';
|
'<p class="text-gray-400 text-center text-lg">Found results, but none had valid images.</p>';
|
||||||
@@ -109,7 +109,15 @@ export async function loadMoreResults(
|
|||||||
|
|
||||||
contentGallery.appendChild(fragment);
|
contentGallery.appendChild(fragment);
|
||||||
|
|
||||||
|
applyLayoutToGallery(contentGallery, currentLayout);
|
||||||
|
|
||||||
hasNextPage = result.data.hasNextPage;
|
hasNextPage = result.data.hasNextPage;
|
||||||
currentPage++;
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search/Load Error:", error);
|
||||||
|
if (loadingSpinner) loadingSpinner.classList.add('hidden');
|
||||||
|
if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden');
|
||||||
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
|
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
|
||||||
removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id),
|
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'),
|
toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'),
|
||||||
|
|
||||||
getSources: () => ipcRenderer.invoke('api:getSources'),
|
getSources: () => ipcRenderer.invoke('api:getSources'),
|
||||||
|
|||||||
205
src/renderer.js
205
src/renderer.js
@@ -4,25 +4,28 @@ import { getDomElements } from './utils/dom-loader.js';
|
|||||||
import { performSearch, loadMoreResults } from './modules/search-handler.js';
|
import { performSearch, loadMoreResults } from './modules/search-handler.js';
|
||||||
import { createImageCard, populateTagModal } from './content/image-handler.js';
|
import { createImageCard, populateTagModal } from './content/image-handler.js';
|
||||||
import { showMessage as uiShowMessage } from './modules/ui-utils.js';
|
import { showMessage as uiShowMessage } from './modules/ui-utils.js';
|
||||||
import { showPage as navShowPage } from './modules/navigation-handler.js';
|
import { applyLayoutToGallery } from './modules/layout-manager.js';
|
||||||
import { applyLayoutToGallery, loadSavedLayout, saveLayout } from './modules/layout-manager.js';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const domRefs = getDomElements();
|
const domRefs = getDomElements();
|
||||||
|
|
||||||
|
const currentLayout = 'grid';
|
||||||
let currentSource = '';
|
let currentSource = '';
|
||||||
let currentLayout = loadSavedLayout();
|
let currentPage = 1;
|
||||||
|
let isFetching = false;
|
||||||
setupGlobalKeybinds(domRefs.searchModal);
|
|
||||||
|
|
||||||
function showMessage(message, type = 'success') {
|
function showMessage(message, type = 'success') {
|
||||||
|
if (domRefs.messageBar) {
|
||||||
uiShowMessage(domRefs.messageBar, message, type);
|
uiShowMessage(domRefs.messageBar, message, type);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showTagModal(tags) {
|
function showTagModal(tags) {
|
||||||
|
if (domRefs.tagInfoContent && domRefs.tagInfoModal) {
|
||||||
populateTagModal(domRefs.tagInfoContent, tags);
|
populateTagModal(domRefs.tagInfoContent, tags);
|
||||||
domRefs.tagInfoModal.classList.remove('hidden');
|
domRefs.tagInfoModal.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) {
|
function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) {
|
||||||
return createImageCard(id, tags, imageUrl, thumbnailUrl, type, {
|
return createImageCard(id, tags, imageUrl, thumbnailUrl, type, {
|
||||||
@@ -30,16 +33,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
showMessage,
|
showMessage,
|
||||||
showTagModal,
|
showTagModal,
|
||||||
applyLayoutToGallery,
|
applyLayoutToGallery,
|
||||||
favoritesGallery: domRefs.favoritesGallery
|
favoritesGallery: document.getElementById('favorites-gallery')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHeader() {
|
function updateHeader() {
|
||||||
if (currentSource) {
|
if (!domRefs.headerContext) return;
|
||||||
domRefs.headerContext.textContent = `Source: ${currentSource}`;
|
domRefs.headerContext.classList.add('hidden');
|
||||||
} else {
|
|
||||||
domRefs.headerContext.textContent = 'No source selected';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const callbacks = {
|
const callbacks = {
|
||||||
@@ -49,10 +49,50 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
createImageCard: localCreateImageCard
|
createImageCard: localCreateImageCard
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleNavigation(pageId) {
|
if (domRefs.searchModal) {
|
||||||
navShowPage(pageId, domRefs, callbacks, { currentLayout });
|
setupGlobalKeybinds(domRefs.searchModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialSource = '';
|
||||||
|
if (window.api && window.api.getSources) {
|
||||||
|
initialSource = await populateSources(domRefs.sourceList);
|
||||||
|
} else {
|
||||||
|
initialSource = await populateSources(domRefs.sourceList);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSource = initialSource;
|
||||||
|
updateHeader();
|
||||||
|
|
||||||
domRefs.sourceList.addEventListener('click', (e) => {
|
domRefs.sourceList.addEventListener('click', (e) => {
|
||||||
const button = e.target.closest('.source-button');
|
const button = e.target.closest('.source-button');
|
||||||
if (button) {
|
if (button) {
|
||||||
@@ -62,71 +102,108 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
button.classList.add('active');
|
button.classList.add('active');
|
||||||
|
|
||||||
currentSource = button.dataset.source;
|
currentSource = button.dataset.source;
|
||||||
console.log('Source changed to:', currentSource);
|
|
||||||
updateHeader();
|
updateHeader();
|
||||||
|
|
||||||
if (domRefs.searchInput.value.trim()) {
|
currentPage = 1;
|
||||||
|
|
||||||
|
if (domRefs.searchInput && domRefs.searchInput.value.trim()) {
|
||||||
performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks);
|
performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks);
|
||||||
|
} else if (domRefs.searchInput) {
|
||||||
|
performSearch(currentSource, { value: "" }, currentLayout, domRefs, callbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
domRefs.browseButton.addEventListener('click', () => handleNavigation('browse-page'));
|
const scrollContainer = document.querySelector('.content-view');
|
||||||
domRefs.favoritesButton.addEventListener('click', () => handleNavigation('favorites-page'));
|
if (scrollContainer) {
|
||||||
domRefs.settingsButton.addEventListener('click', () => handleNavigation('settings-page'));
|
scrollContainer.addEventListener('scroll', async () => {
|
||||||
|
|
||||||
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 (
|
if (
|
||||||
domRefs.browsePage.scrollTop + domRefs.browsePage.clientHeight >=
|
scrollContainer.scrollTop + scrollContainer.clientHeight >=
|
||||||
domRefs.browsePage.scrollHeight - 600
|
scrollContainer.scrollHeight - 600
|
||||||
) {
|
) {
|
||||||
loadMoreResults(currentSource, currentLayout, domRefs, callbacks);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
domRefs.layoutRadios.forEach((radio) => {
|
if (domRefs.searchButton && domRefs.searchInput) {
|
||||||
radio.addEventListener('change', (e) => {
|
domRefs.searchButton.addEventListener('click', () => {
|
||||||
const newLayout = e.target.value;
|
currentPage = 1;
|
||||||
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);
|
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 {
|
} else {
|
||||||
applyLayoutToGallery(domRefs.contentGallery, currentLayout);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const initialSource = await populateSources(domRefs.sourceList);
|
|
||||||
currentSource = initialSource;
|
|
||||||
|
|
||||||
updateHeader();
|
|
||||||
handleNavigation('browse-page');
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,14 @@ const { BrowserWindow } = require('electron');
|
|||||||
|
|
||||||
class HeadlessBrowser {
|
class HeadlessBrowser {
|
||||||
async scrape(url, evalFunc, options = {}) {
|
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({
|
const win = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
@@ -12,7 +19,7 @@ class HeadlessBrowser {
|
|||||||
offscreen: true,
|
offscreen: true,
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
images: false,
|
images: loadImages,
|
||||||
webgl: false,
|
webgl: false,
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
},
|
},
|
||||||
@@ -23,32 +30,37 @@ class HeadlessBrowser {
|
|||||||
win.webContents.setUserAgent(userAgent);
|
win.webContents.setUserAgent(userAgent);
|
||||||
|
|
||||||
const session = win.webContents.session;
|
const session = win.webContents.session;
|
||||||
const filter = { urls: ['*://*/*'] };
|
session.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => {
|
||||||
|
|
||||||
session.webRequest.onBeforeRequest(filter, (details, callback) => {
|
|
||||||
const url = details.url.toLowerCase();
|
const url = details.url.toLowerCase();
|
||||||
|
|
||||||
const blockExtensions = [
|
const blockExtensions = [
|
||||||
'.css', '.woff', '.woff2', '.ttf', '.svg', '.eot',
|
'.woff', '.woff2', '.ttf', '.eot',
|
||||||
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem'
|
'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem'
|
||||||
];
|
];
|
||||||
|
if (blockExtensions.some(ext => url.includes(ext))) return callback({ cancel: true });
|
||||||
const isBlocked = blockExtensions.some(ext => url.includes(ext));
|
|
||||||
|
|
||||||
if (isBlocked) {
|
|
||||||
return callback({ cancel: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback({ cancel: false });
|
return callback({ cancel: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
await win.loadURL(url, { userAgent });
|
await win.loadURL(url, { userAgent });
|
||||||
|
|
||||||
if (waitSelector) {
|
if (waitSelector) {
|
||||||
|
try {
|
||||||
await this.waitForSelector(win, waitSelector, timeout);
|
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;
|
return result;
|
||||||
|
|
||||||
@@ -70,11 +82,12 @@ class HeadlessBrowser {
|
|||||||
}, ${timeout});
|
}, ${timeout});
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
if (document.querySelector('${selector}')) {
|
const el = document.querySelector('${selector}');
|
||||||
|
if (el) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(check, 50);
|
setTimeout(check, 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
check();
|
check();
|
||||||
@@ -82,6 +95,30 @@ class HeadlessBrowser {
|
|||||||
`;
|
`;
|
||||||
await win.webContents.executeJavaScript(script);
|
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();
|
module.exports = new HeadlessBrowser();
|
||||||
94
views/favorites.html
Normal file
94
views/favorites.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https:;" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Waifu Board - Favorites</title>
|
||||||
|
<link rel="stylesheet" href="styles/home.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<br>
|
||||||
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<a href="index.html" class="nav-button" title="Image Boards">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
<span>Image Boards</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="favorites.html" class="nav-button active" title="Favorites">
|
||||||
|
<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>
|
||||||
|
<span>Favorites</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: auto;">
|
||||||
|
<a href="settings.html" class="nav-button" title="Settings">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<header class="top-header">
|
||||||
|
<div style="position: relative;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-tertiary);">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<input type="text" id="search-input" placeholder="Search across sources..." style="padding-left: 2.5rem;" />
|
||||||
|
</div>
|
||||||
|
<button id="search-button" class="hidden"></button>
|
||||||
|
<button id="search-icon-button" class="hidden"></button>
|
||||||
|
<button id="search-close-button" class="hidden"></button>
|
||||||
|
<div id="header-context" class="hidden"></div>
|
||||||
|
<h1 id="page-title" class="hidden">Favorites</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<div id="favorites-page" class="page">
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<h2 style="margin-bottom: 0.5rem;">Your Favorites</h2>
|
||||||
|
<p style="color: var(--text-secondary);">Your personally curated collection.</p>
|
||||||
|
</div>
|
||||||
|
<main id="favorites-gallery" class="gallery-masonry"></main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tag-info-modal" class="hidden">
|
||||||
|
<div>
|
||||||
|
<button id="tag-info-close-button">×</button>
|
||||||
|
<h3 style="margin-top:0; margin-bottom: 1rem;">Tags</h3>
|
||||||
|
<div id="tag-info-content" class="tag-cloud"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-bar" class="toast hidden">Message</div>
|
||||||
|
|
||||||
|
<script type="module" src="../src/renderer.js"></script>
|
||||||
|
<script type="module" src="../scripts/main.js"></script>
|
||||||
|
<script src="../src/updateNotification.js"></script>
|
||||||
|
<script>
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const searchBtn = document.getElementById('search-button');
|
||||||
|
if(searchInput && searchBtn) {
|
||||||
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') searchBtn.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
124
views/index.html
124
views/index.html
@@ -1,86 +1,68 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https:;" />
|
||||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https:;" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Waifu Board</title>
|
<title>Waifu Board</title>
|
||||||
<link rel="stylesheet" href="./styles/home.css">
|
<link rel="stylesheet" href="styles/home.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<br>
|
<br>
|
||||||
<nav style="display: flex; flex-direction: column; gap: 0.5rem;">
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
<button id="browse-button" class="nav-button active" title="Browse">
|
<a href="index.html" class="nav-button active" title="Image Boards">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="3" y="3" width="7" height="7"></rect>
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
<rect x="14" y="3" width="7" height="7"></rect>
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
<rect x="14" y="14" width="7" height="7"></rect>
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
<rect x="3" y="14" width="7" height="7"></rect>
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Browse</span>
|
<span>Image Boards</span>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
<button id="favorites-button" class="nav-button" title="Favorites">
|
<a href="favorites.html" class="nav-button" title="Favorites">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path
|
<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>
|
||||||
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>
|
</svg>
|
||||||
<span>Favorites</span>
|
<span>Favorites</span>
|
||||||
</button>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: auto;">
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: auto;">
|
||||||
<button id="settings-button" class="nav-button" title="Settings">
|
<a href="settings.html" class="nav-button" title="Settings">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
<path
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
|
|
||||||
</path>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</button>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
|
|
||||||
<header class="top-header">
|
<header class="top-header">
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-tertiary);">
|
||||||
style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-tertiary);">
|
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text" id="search-input" placeholder="Search across sources..."
|
<input type="text" id="search-input" placeholder="Search across sources..." style="padding-left: 2.5rem;" />
|
||||||
style="padding-left: 2.5rem;" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="search-button" class="hidden"></button>
|
<button id="search-button" class="hidden"></button>
|
||||||
<button id="search-icon-button" class="hidden"></button>
|
<button id="search-icon-button" class="hidden"></button>
|
||||||
<button id="search-close-button" class="hidden"></button>
|
<button id="search-close-button" class="hidden"></button>
|
||||||
<div id="header-context" class="hidden"></div>
|
<div id="header-context" class="hidden"></div>
|
||||||
<h1 id="page-title" class="hidden">Home</h1>
|
<h1 id="page-title" class="hidden">Dashboard</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content-view">
|
<div class="content-view">
|
||||||
|
|
||||||
<div id="browse-page" class="page">
|
<div id="browse-page" class="page">
|
||||||
|
<h3 style="color: var(--text-tertiary); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 1rem;">Sources</h3>
|
||||||
|
<div id="source-list"></div>
|
||||||
|
|
||||||
<h3
|
<h3 style="color: var(--text-tertiary); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin: 2rem 0 1rem 0;">Library</h3>
|
||||||
style="color: var(--text-tertiary); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 1rem;">
|
|
||||||
Sources</h3>
|
|
||||||
<div id="source-list">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
style="color: var(--text-tertiary); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin: 2rem 0 1rem 0;">
|
|
||||||
Library</h3>
|
|
||||||
<main id="content-gallery" class="gallery-masonry">
|
<main id="content-gallery" class="gallery-masonry">
|
||||||
<div id="gallery-placeholder" class="loading-state" style="width: 100%; grid-column: 1 / -1;">
|
<div id="gallery-placeholder" class="loading-state" style="width: 100%; grid-column: 1 / -1;">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#52525b" stroke-width="1">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#52525b" stroke-width="1">
|
||||||
@@ -90,70 +72,17 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<p>Select a source above to load content</p>
|
<p>Select a source above to load content</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading-spinner" class="hidden loading-state" style="width: 100%; grid-column: 1 / -1;">
|
<div id="loading-spinner" class="hidden loading-state" style="width: 100%; grid-column: 1 / -1;">
|
||||||
<svg style="width:32px; height:32px; color: var(--accent); animation: spin 1s linear infinite;"
|
<svg style="width:32px; height:32px; color: var(--accent); animation: spin 1s linear infinite;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"></path>
|
||||||
<path
|
|
||||||
d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83">
|
|
||||||
</path>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p>Fetching images...</p>
|
<p>Fetching images...</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="infinite-loading-spinner" class="hidden loading-state"
|
<div id="infinite-loading-spinner" class="hidden loading-state" style="width: 100%; grid-column: 1 / -1;">
|
||||||
style="width: 100%; grid-column: 1 / -1;">
|
|
||||||
<p>Loading more...</p>
|
<p>Loading more...</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="favorites-page" class="page hidden">
|
|
||||||
<div style="margin-bottom: 2rem;">
|
|
||||||
<h2 style="margin-bottom: 0.5rem;">Your Favorites</h2>
|
|
||||||
<p style="color: var(--text-secondary);">Your personally curated collection.</p>
|
|
||||||
</div>
|
|
||||||
<main id="favorites-gallery" class="gallery-masonry"></main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settings-page" class="page hidden">
|
|
||||||
<div style="margin-bottom: 2rem;">
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<p style="color: var(--text-secondary);">Customize your viewing experience.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Layout Style</h3>
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="layout" value="scroll" id="layout-scroll">
|
|
||||||
<div>
|
|
||||||
<strong>Scroll View</strong>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--text-tertiary);">Single column feed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="layout" value="grid" id="layout-grid">
|
|
||||||
<div>
|
|
||||||
<strong>Masonry Grid</strong>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--text-tertiary);">Staggered dynamic
|
|
||||||
heights</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="layout" value="compact" id="layout-compact">
|
|
||||||
<div>
|
|
||||||
<strong>Compact Grid</strong>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--text-tertiary);">Uniform square tiles
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,31 +95,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="search-modal" class="hidden"></div>
|
<div id="search-modal" class="hidden"></div>
|
||||||
|
|
||||||
<div id="message-bar" class="toast hidden">Message</div>
|
<div id="message-bar" class="toast hidden">Message</div>
|
||||||
<div id="updateToast" class="toast hidden" style="border-left-color: #eab308;">
|
<div id="updateToast" class="toast hidden" style="border-left-color: #eab308;">
|
||||||
<p>Update Available: <span id="latestVersionDisplay">v1.x</span></p>
|
<p>Update Available: <span id="latestVersionDisplay">v1.x</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script type="module" src="../src/renderer.js"></script>
|
<script type="module" src="../src/renderer.js"></script>
|
||||||
<script type="module" src="../scripts/main.js"></script>
|
<script type="module" src="../scripts/main.js"></script>
|
||||||
<script src="../src/updateNotification.js"></script>
|
<script src="../src/updateNotification.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Simple Enter key handler for search
|
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const searchBtn = document.getElementById('search-button');
|
const searchBtn = document.getElementById('search-button');
|
||||||
|
if(searchInput && searchBtn) {
|
||||||
searchInput.addEventListener('keydown', (e) => {
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') searchBtn.click();
|
if (e.key === 'Enter') searchBtn.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
91
views/settings.html
Normal file
91
views/settings.html
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https:;" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Waifu Board - Settings</title>
|
||||||
|
<link rel="stylesheet" href="styles/home.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<br>
|
||||||
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<a href="index.html" class="nav-button" title="Image Boards">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
<span>Image Boards</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="favorites.html" class="nav-button" title="Favorites">
|
||||||
|
<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>
|
||||||
|
<span>Favorites</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav style="display: flex; flex-direction: column; gap: 0.5rem; margin-top: auto;">
|
||||||
|
<a href="settings.html" class="nav-button active" title="Settings">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Settings</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<header class="top-header">
|
||||||
|
<div style="position: relative;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="position: absolute; left: 15px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-tertiary);">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<input type="text" id="search-input" placeholder="Search across sources..." style="padding-left: 2.5rem;" />
|
||||||
|
</div>
|
||||||
|
<button id="search-button" class="hidden"></button>
|
||||||
|
<button id="search-icon-button" class="hidden"></button>
|
||||||
|
<button id="search-close-button" class="hidden"></button>
|
||||||
|
<div id="header-context" class="hidden"></div>
|
||||||
|
<h1 id="page-title" class="hidden">Settings</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<div id="settings-page" class="page">
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<p style="color: var(--text-secondary);">App configuration.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-card">
|
||||||
|
<p style="color: var(--text-tertiary); font-style: italic;">No settings available currently.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-bar" class="toast hidden">Message</div>
|
||||||
|
|
||||||
|
<script type="module" src="../src/renderer.js"></script>
|
||||||
|
<script type="module" src="../scripts/main.js"></script>
|
||||||
|
<script src="../src/updateNotification.js"></script>
|
||||||
|
<script>
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const searchBtn = document.getElementById('search-button');
|
||||||
|
if(searchInput && searchBtn) {
|
||||||
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') searchBtn.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -136,6 +136,9 @@ body {
|
|||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a, a:visited, a:hover, a:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-button span {
|
.nav-button span {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user