Organized all the code

Updated the update notification to check periodically every 5 minutes
Added in headless browser support for extensions (check the extensions repo to see an example)
Added in DiscordRPC support
This commit is contained in:
2025-11-19 16:28:33 -05:00
parent 2f556c2ddc
commit 5f3020ca6e
24 changed files with 1077 additions and 815 deletions

View File

@@ -0,0 +1,115 @@
import { createAddFavoriteButton, createRemoveFavoriteButton } from '../favorites/favorites-handler.js';
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.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;
}

74
src/database/db-init.js Normal file
View File

@@ -0,0 +1,74 @@
const sqlite3 = require('sqlite3').verbose();
function runDatabaseMigrations(db) {
db.serialize(() => {
db.run(
`
CREATE TABLE IF NOT EXISTS favorites (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
image_url TEXT NOT NULL,
thumbnail_url TEXT NOT NULL DEFAULT "",
tags TEXT NOT NULL DEFAULT ""
)
`,
(err) => {
if (err) console.error('Error creating table:', err.message);
}
);
console.log('Checking database schema for "thumbnail_url"...');
db.all('PRAGMA table_info(favorites)', (err, columns) => {
if (err) return console.error('Failed to get table info:', err.message);
const hasThumbnailColumn = columns.some((col) => col.name === 'thumbnail_url');
if (!hasThumbnailColumn) {
console.log('MIGRATION: Adding "thumbnail_url" column...');
db.run(
'ALTER TABLE favorites ADD COLUMN thumbnail_url TEXT NOT NULL DEFAULT ""',
(err) => {
if (err) console.error('Migration error (thumbnail_url):', err.message);
else console.log('MIGRATION: "thumbnail_url" added successfully.');
}
);
} else {
console.log('"thumbnail_url" column is up-to-date.');
}
});
console.log('Checking database schema for "tags" column...');
db.all('PRAGMA table_info(favorites)', (err, columns) => {
if (err) return console.error('Failed to get table info:', err.message);
const hasTagsColumn = columns.some((col) => col.name === 'tags');
if (!hasTagsColumn) {
console.log('MIGRATION: Adding "tags" column...');
db.run(
'ALTER TABLE favorites ADD COLUMN tags TEXT NOT NULL DEFAULT ""',
(err) => {
if (err) console.error('Migration error (tags):', err.message);
else console.log('MIGRATION: "tags" column added successfully.');
}
);
} else {
console.log('"tags" column is up-to-date.');
}
});
});
}
function initDatabase(dbPath) {
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database:', err.message);
} else {
console.log('Connected to the favorites database.');
runDatabaseMigrations(db);
}
});
return db;
}
module.exports = initDatabase;

82
src/discord-rpc.js Normal file
View File

@@ -0,0 +1,82 @@
const { DiscordRPCClient } = require('@ryuziii/discord-rpc');
require('dotenv').config();
let rpcClient;
let reconnectTimer;
function initDiscordRPC() {
const clientId = process.env.DISCORD_CLIENT_ID;
if (!clientId) {
console.warn('Discord RPC: Invalid or missing Client ID. Check your .env file.');
return;
}
console.log(`Discord RPC: Initializing with Client ID ending in ...${clientId.slice(-4)}`);
if (rpcClient) {
try { rpcClient.destroy(); } catch (e) {}
rpcClient = null;
}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
try {
rpcClient = new DiscordRPCClient({
clientId: clientId,
transport: 'ipc'
});
} catch (err) {
console.error('Discord RPC: Failed to instantiate Client.', err);
return;
}
rpcClient.on('ready', () => {
const user = rpcClient.user ? rpcClient.user.username : 'User';
console.log(`Discord RPC: Authed for user ${user}`);
setTimeout(() => {
setActivity();
}, 1000);
});
rpcClient.on('disconnected', () => {
console.log('Discord RPC: Disconnected. Attempting to reconnect in 10s...');
if (!reconnectTimer) {
reconnectTimer = setTimeout(() => {
initDiscordRPC();
}, 10000);
}
});
try {
rpcClient.connect().catch(err => {
console.error('Discord RPC: Connection failed', err.message);
});
} catch (err) {
console.error('Discord RPC: Error initializing', err);
}
}
function setActivity() {
if (!rpcClient) return;
try {
const activity = {
details: 'Browsing',
state: 'In App',
startTimestamp: new Date(),
largeImageKey: 'bigpicture',
instance: false,
};
rpcClient.setActivity(activity);
console.log('Discord RPC: Activity set successfully');
} catch (error) {
console.error("Discord RPC: Failed to set activity", error);
}
}
module.exports = { initDiscordRPC };

View File

@@ -0,0 +1,56 @@
export async function populateSources(sourceList) {
console.log('Requesting sources from main process...');
const sources = await window.api.getSources();
sourceList.innerHTML = '';
let initialSource = '';
if (sources && sources.length > 0) {
sources.forEach((source) => {
const button = document.createElement('button');
button.className =
'source-button w-12 h-12 flex items-center justify-center rounded-xl text-gray-400 hover:bg-gray-700 hover:text-white transition-all duration-200';
button.dataset.source = source.name;
button.title = source.name;
const favicon = document.createElement('img');
favicon.className = 'w-8 h-8 rounded';
let mainDomain = source.url;
try {
const hostname = new URL(source.url).hostname;
const parts = hostname.split('.');
if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
mainDomain = parts.slice(1).join('.');
} else {
mainDomain = hostname;
}
} catch (e) {
console.warn(`Could not parse domain from ${source.url}:`, e);
mainDomain = source.name;
}
favicon.src = `https://www.google.com/s2/favicons?domain=${mainDomain}&sz=32`;
favicon.alt = source.name;
favicon.onerror = () => {
button.innerHTML = `<span class="font-bold text-sm">${source.name.substring(
0,
2
)}</span>`;
favicon.remove();
};
button.appendChild(favicon);
sourceList.appendChild(button);
});
console.log('Sources populated:', sources);
if (sourceList.children.length > 0) {
const firstButton = sourceList.children[0];
firstButton.classList.add('active');
initialSource = firstButton.dataset.source;
}
} else {
console.warn('No sources were loaded from the main process.');
}
return initialSource;
}

View File

@@ -0,0 +1,76 @@
export async function handleAddFavorite(id, tags, imageUrl, thumbnailUrl, showMessageCallback) {
const safeTags = Array.isArray(tags) ? tags : [];
const title = safeTags.length > 0 ? safeTags[0] : 'Favorite';
const allTags = safeTags.join(',');
const result = await window.api.addFavorite({
id,
title,
imageUrl,
thumbnailUrl,
tags: allTags,
});
if (result.success) {
showMessageCallback('Added to favorites!', 'success');
} else {
showMessageCallback(result.error, 'error');
}
}
export async function handleRemoveFavorite(id, favoritesGallery, showMessageCallback, applyLayoutCallback, currentLayout) {
const result = await window.api.removeFavorite(id);
if (result.success) {
showMessageCallback('Removed from favorites.', 'success');
const cardToRemove = document.querySelector(`#favorites-gallery [data-id='${id}']`);
if (cardToRemove) {
cardToRemove.classList.add('opacity-0', 'scale-90');
setTimeout(() => {
cardToRemove.remove();
if (favoritesGallery.children.length === 0) {
applyLayoutCallback(favoritesGallery, currentLayout);
favoritesGallery.innerHTML =
'<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
}
}, 300);
}
} else {
showMessageCallback(result.error, 'error');
}
}
export function createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl, showMessageCallback) {
const button = document.createElement('button');
button.title = 'Add to Favorites';
button.className =
'p-2 rounded-full bg-black/50 text-white hover:bg-indigo-600 backdrop-blur-sm transition-colors';
button.innerHTML = `<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.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.31h5.518a.562.562 0 01.31.95l-4.203 3.03a.563.563 0 00-.182.53l1.501 4.87a.562.562 0 01-.82.624l-4.204-3.03a.563.563 0 00-.576 0l-4.204 3.03a.562.562 0 01-.82-.624l1.501-4.87a.563.563 0 00-.182-.53L2.498 9.87a.562.562 0 01.31-.95h5.518a.563.563 0 00.475-.31L11.48 3.5z" />
</svg>`;
button.onclick = (e) => {
e.stopPropagation();
handleAddFavorite(id, safeTags, imageUrl, thumbnailUrl, showMessageCallback);
};
return button;
}
export function createRemoveFavoriteButton(id, favoritesGallery, showMessageCallback, applyLayoutCallback, currentLayout) {
const button = document.createElement('button');
button.title = 'Remove from Favorites';
button.className =
'p-2 rounded-full bg-black/50 text-white hover:bg-red-600 backdrop-blur-sm transition-colors';
button.innerHTML = `<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.578 0a48.108 48.108 0 01-3.478-.397m15.408 0l-2.147-2.147A1.125 1.125 0 0016.34 3H7.66a1.125 1.125 0 00-.795.325L4.772 5.79m14.456 0l-2.29-2.29a1.125 1.125 0 00-.795-.324H8.455a1.125 1.125 0 00-.795.324L5.37 5.79m13.84 0L20.25 7.5" />
</svg>`;
button.onclick = (e) => {
e.stopPropagation();
handleRemoveFavorite(id, favoritesGallery, showMessageCallback, applyLayoutCallback, currentLayout);
};
return button;
}

View File

@@ -0,0 +1,33 @@
export async function loadFavorites(favoritesGallery, currentLayout, applyLayoutCallback, createImageCardCallback) {
applyLayoutCallback(favoritesGallery, currentLayout);
favoritesGallery.innerHTML =
'<div class="text-center p-10"><p class="text-gray-400">Loading favorites...</p></div>';
const currentFavorites = await window.api.getFavorites();
if (currentFavorites.length === 0) {
applyLayoutCallback(favoritesGallery, currentLayout);
favoritesGallery.innerHTML =
'<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
return;
}
applyLayoutCallback(favoritesGallery, currentLayout);
favoritesGallery.innerHTML = '';
const fragment = document.createDocumentFragment();
currentFavorites.forEach((fav) => {
const card = createImageCardCallback(
fav.id,
fav.tags ? fav.tags.split(',') : [],
fav.image_url,
fav.thumbnail_url,
'fav'
);
fragment.appendChild(card);
});
favoritesGallery.appendChild(fragment);
}

29
src/ipc/api-handlers.js Normal file
View File

@@ -0,0 +1,29 @@
module.exports = function (loadedScrapers) {
return {
getSources: () => {
return Object.keys(loadedScrapers).map((name) => {
return {
name: name,
url: loadedScrapers[name].baseUrl,
};
});
},
search: async (event, source, query, page) => {
try {
if (loadedScrapers[source] && loadedScrapers[source].instance) {
const results = await loadedScrapers[source].instance.fetchSearchResult(
query,
page
);
return { success: true, data: results };
} else {
throw new Error(`Unknown source or source failed to load: ${source}`);
}
} catch (error) {
console.error(`Error searching ${source}:`, error);
return { success: false, error: error.message };
}
},
};
};

53
src/ipc/db-handlers.js Normal file
View File

@@ -0,0 +1,53 @@
module.exports = function (db) {
return {
getFavorites: () => {
return new Promise((resolve, reject) => {
db.all('SELECT * FROM favorites', [], (err, rows) => {
if (err) {
console.error('Error getting favorites:', err.message);
resolve([]);
} else {
resolve(rows);
}
});
});
},
addFavorite: (event, fav) => {
return new Promise((resolve) => {
const stmt =
'INSERT INTO favorites (id, title, image_url, thumbnail_url, tags) VALUES (?, ?, ?, ?, ?)';
db.run(
stmt,
[fav.id, fav.title, fav.imageUrl, fav.thumbnailUrl, fav.tags],
function (err) {
if (err) {
if (err.code.includes('SQLITE_CONSTRAINT')) {
resolve({ success: false, error: 'Item is already a favorite.' });
} else {
console.error('Error adding favorite:', err.message);
resolve({ success: false, error: err.message });
}
} else {
resolve({ success: true, id: fav.id });
}
}
);
});
},
removeFavorite: (event, id) => {
return new Promise((resolve) => {
const stmt = 'DELETE FROM favorites WHERE id = ?';
db.run(stmt, id, function (err) {
if (err) {
console.error('Error removing favorite:', err.message);
resolve({ success: false, error: err.message });
} else {
resolve({ success: this.changes > 0 });
}
});
});
},
};
};

View File

@@ -0,0 +1,32 @@
export function applyLayoutToGallery(galleryElement, layout) {
if (!galleryElement) return;
galleryElement.className = 'p-4 w-full';
if (layout === 'scroll') {
galleryElement.classList.add('max-w-3xl', 'mx-auto', 'space-y-8');
} else if (layout === 'grid') {
galleryElement.classList.add('gallery-masonry');
} else if (layout === 'compact') {
galleryElement.classList.add('gallery-grid');
}
}
export function loadSavedLayout() {
const savedLayout = localStorage.getItem('waifuBoardLayout') || 'scroll';
const savedRadio = document.querySelector(`input[name="layout"][value="${savedLayout}"]`);
if (savedRadio) {
savedRadio.checked = true;
} else {
const defaultRadio = document.getElementById('layout-scroll');
if(defaultRadio) defaultRadio.checked = true;
}
return savedLayout;
}
export function saveLayout(newLayout) {
localStorage.setItem('waifuBoardLayout', newLayout);
console.log('Layout changed to:', newLayout);
}

View File

@@ -0,0 +1,52 @@
import { loadFavorites } from '../favorites/favorites-loader.js';
export function showPage(pageId, domRefs, callbacks, state) {
const {
browseButton,
favoritesButton,
settingsButton,
pageTitle,
headerContext,
favoritesGallery
} = domRefs;
const { updateHeader, applyLayoutToGallery, createImageCard } = callbacks;
const { currentLayout } = state;
document.querySelectorAll('.page').forEach((page) => {
page.classList.add('hidden');
});
document.querySelectorAll('.nav-button').forEach((tab) => {
tab.classList.remove('bg-indigo-600', 'text-white');
tab.classList.add('text-gray-400', 'hover:bg-gray-700');
});
const activePage = document.getElementById(pageId);
if (activePage) {
activePage.classList.remove('hidden');
}
let activeTab;
if (pageId === 'browse-page') {
activeTab = browseButton;
pageTitle.textContent = 'Browse';
updateHeader();
} else if (pageId === 'favorites-page') {
activeTab = favoritesButton;
pageTitle.textContent = 'Favorites';
headerContext.textContent = '';
loadFavorites(favoritesGallery, currentLayout, applyLayoutToGallery, createImageCard);
} else if (pageId === 'settings-page') {
activeTab = settingsButton;
pageTitle.textContent = 'Settings';
headerContext.textContent = '';
}
if (activeTab) {
activeTab.classList.add('bg-indigo-600', 'text-white');
activeTab.classList.remove('text-gray-400', 'hover:bg-gray-700');
}
}

View File

@@ -0,0 +1,115 @@
let currentPage = 1;
let hasNextPage = true;
let isLoading = false;
let currentQuery = '';
export async function performSearch(
currentSource,
searchInput,
currentLayout,
domRefs,
callbacks
) {
const { showMessage, applyLayoutToGallery, updateHeader } = callbacks;
const { galleryPlaceholder, contentGallery, searchModal } = domRefs;
if (!currentSource) {
showMessage('Please select a source from the sidebar.', 'error');
return;
}
currentPage = 1;
hasNextPage = true;
isLoading = false;
currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' ');
if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
applyLayoutToGallery(contentGallery, currentLayout);
contentGallery.innerHTML = '';
updateHeader();
searchModal.classList.add('hidden');
await loadMoreResults(currentSource, currentLayout, domRefs, callbacks);
}
export async function loadMoreResults(
currentSource,
currentLayout,
domRefs,
callbacks
) {
const { loadingSpinner, infiniteLoadingSpinner, contentGallery } = domRefs;
const { applyLayoutToGallery, createImageCard } = callbacks;
if (isLoading || !hasNextPage) {
return;
}
isLoading = true;
if (currentPage === 1) {
loadingSpinner.classList.remove('hidden');
} else {
infiniteLoadingSpinner.classList.remove('hidden');
}
const result = await window.api.search(
currentSource,
currentQuery,
currentPage
);
loadingSpinner.classList.add('hidden');
infiniteLoadingSpinner.classList.add('hidden');
if (
!result.success ||
!result.data.results ||
result.data.results.length === 0
) {
hasNextPage = false;
if (currentPage === 1) {
applyLayoutToGallery(contentGallery, currentLayout);
contentGallery.innerHTML =
'<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) {
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;
}

18
src/modules/ui-utils.js Normal file
View File

@@ -0,0 +1,18 @@
export function showMessage(messageBar, message, type = 'success') {
if (!messageBar) return;
messageBar.textContent = message;
if (type === 'error') {
messageBar.classList.remove('bg-green-600');
messageBar.classList.add('bg-red-600');
} else {
messageBar.classList.remove('bg-red-600');
messageBar.classList.add('bg-green-600');
}
messageBar.classList.remove('hidden', 'translate-y-16');
setTimeout(() => {
messageBar.classList.add('hidden', 'translate-y-16');
}, 3000);
}

12
src/preload.js Normal file
View File

@@ -0,0 +1,12 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
getFavorites: () => ipcRenderer.invoke('db:getFavorites'),
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id),
search: (source, query) => ipcRenderer.invoke('api:search', source, query),
getSources: () => ipcRenderer.invoke('api:getSources'),
});

132
src/renderer.js Normal file
View File

@@ -0,0 +1,132 @@
import { populateSources } from './extensions/load-extensions.js';
import { setupGlobalKeybinds } from './utils/keybinds.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';
document.addEventListener('DOMContentLoaded', async () => {
const domRefs = getDomElements();
let currentSource = '';
let currentLayout = loadSavedLayout();
setupGlobalKeybinds(domRefs.searchModal);
function showMessage(message, type = 'success') {
uiShowMessage(domRefs.messageBar, message, type);
}
function showTagModal(tags) {
populateTagModal(domRefs.tagInfoContent, tags);
domRefs.tagInfoModal.classList.remove('hidden');
}
function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) {
return createImageCard(id, tags, imageUrl, thumbnailUrl, type, {
currentLayout,
showMessage,
showTagModal,
applyLayoutToGallery,
favoritesGallery: domRefs.favoritesGallery
});
}
function updateHeader() {
if (currentSource) {
domRefs.headerContext.textContent = `Source: ${currentSource}`;
} else {
domRefs.headerContext.textContent = 'No source selected';
}
}
const callbacks = {
showMessage,
applyLayoutToGallery,
updateHeader,
createImageCard: localCreateImageCard
};
function handleNavigation(pageId) {
navShowPage(pageId, domRefs, callbacks, { currentLayout });
}
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);
}
}
});
});
const initialSource = await populateSources(domRefs.sourceList);
currentSource = initialSource;
updateHeader();
handleNavigation('browse-page');
});

87
src/updateNotification.js Normal file
View File

@@ -0,0 +1,87 @@
const GITHUB_OWNER = 'ItsSkaiya';
const GITHUB_REPO = 'WaifuBoard';
const CURRENT_VERSION = 'v1.3.0';
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
let currentVersionDisplay;
let latestVersionDisplay;
let updateToast;
document.addEventListener('DOMContentLoaded', () => {
currentVersionDisplay = document.getElementById('currentVersionDisplay');
latestVersionDisplay = document.getElementById('latestVersionDisplay');
updateToast = document.getElementById('updateToast');
if (currentVersionDisplay) {
currentVersionDisplay.textContent = CURRENT_VERSION;
}
checkForUpdates();
setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL);
});
function showToast(latestVersion) {
if (latestVersionDisplay && updateToast) {
latestVersionDisplay.textContent = latestVersion;
updateToast.classList.add('update-available');
updateToast.classList.remove('hidden');
} else {
console.error("Error: Cannot display toast because one or more DOM elements were not found.");
}
}
function hideToast() {
if (updateToast) {
updateToast.classList.add('hidden');
updateToast.classList.remove('update-available');
}
}
function isVersionOutdated(versionA, versionB) {
const vA = versionA.replace(/^v/, '').split('.').map(Number);
const vB = versionB.replace(/^v/, '').split('.').map(Number);
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
const numA = vA[i] || 0;
const numB = vB[i] || 0;
if (numA < numB) return true;
if (numA > numB) return false;
}
return false;
}
async function checkForUpdates() {
console.log(`Checking for updates for ${GITHUB_OWNER}/${GITHUB_REPO}...`);
const apiUrl = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.statusText}`);
}
const data = await response.json();
const latestVersion = data.tag_name;
console.log(`Latest GitHub Release: ${latestVersion}`);
if (isVersionOutdated(CURRENT_VERSION, latestVersion)) {
console.warn('Update available!');
showToast(latestVersion);
} else {
console.info('Package is up to date.');
hideToast();
}
} catch (error) {
console.error('Failed to fetch GitHub release:', error);
}
}

26
src/utils/dom-loader.js Normal file
View File

@@ -0,0 +1,26 @@
export function getDomElements() {
return {
browseButton: document.getElementById('browse-button'),
favoritesButton: document.getElementById('favorites-button'),
settingsButton: document.getElementById('settings-button'),
browsePage: document.getElementById('browse-page'),
pageTitle: document.getElementById('page-title'),
headerContext: document.getElementById('header-context'),
searchIconButton: document.getElementById('search-icon-button'),
searchModal: document.getElementById('search-modal'),
searchCloseButton: document.getElementById('search-close-button'),
searchInput: document.getElementById('search-input'),
searchButton: document.getElementById('search-button'),
sourceList: document.getElementById('source-list'),
contentGallery: document.getElementById('content-gallery'),
favoritesGallery: document.getElementById('favorites-gallery'),
loadingSpinner: document.getElementById('loading-spinner'),
infiniteLoadingSpinner: document.getElementById('infinite-loading-spinner'),
messageBar: document.getElementById('message-bar'),
galleryPlaceholder: document.getElementById('gallery-placeholder'),
layoutRadios: document.querySelectorAll('input[name="layout"]'),
tagInfoModal: document.getElementById('tag-info-modal'),
tagInfoCloseButton: document.getElementById('tag-info-close-button'),
tagInfoContent: document.getElementById('tag-info-content'),
};
}

View File

@@ -0,0 +1,67 @@
const { BrowserWindow } = require('electron');
class HeadlessBrowser {
async scrape(url, evalFunc, options = {}) {
const { waitSelector = null, timeout = 15000 } = options;
const win = new BrowserWindow({
show: false,
width: 800,
height: 600,
webPreferences: {
offscreen: true,
contextIsolation: false,
nodeIntegration: false,
images: true,
webgl: false,
},
});
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);
await win.loadURL(url, { userAgent });
if (waitSelector) {
await this.waitForSelector(win, waitSelector, timeout);
}
const result = await win.webContents.executeJavaScript(`(${evalFunc.toString()})()`);
return result;
} catch (error) {
console.error('Headless Scrape Error:', error.message);
throw error;
} finally {
if (!win.isDestroyed()) {
win.destroy();
}
}
}
async waitForSelector(win, selector, timeout) {
const script = `
new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout waiting for selector: ${selector}'));
}, ${timeout});
const check = () => {
if (document.querySelector('${selector}')) {
clearTimeout(timer);
resolve(true);
} else {
// FIX: Use setTimeout because requestAnimationFrame stops in hidden windows
setTimeout(check, 100);
}
};
check();
});
`;
await win.webContents.executeJavaScript(script);
}
}
module.exports = new HeadlessBrowser();

7
src/utils/keybinds.js Normal file
View File

@@ -0,0 +1,7 @@
export function setupGlobalKeybinds(searchModal) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchModal.classList.add('hidden');
}
});
}