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

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/node_modules
/dist
/dist
.env

182
main.js
View File

@@ -1,7 +1,11 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const initDatabase = require('./src/database/db-init');
const { initDiscordRPC } = require('./src/discord-rpc');
const headlessBrowser = require('./src/utils/headless-browser');
const fetchPath = require.resolve('node-fetch');
const cheerioPath = require.resolve('cheerio');
@@ -15,12 +19,10 @@ try {
fs.mkdirSync(waifuBoardsPath);
}
if (!fs.existsSync(pluginsPath)) {
fs.mkdirSync(pluginsPath, { recursive: true });
}
} catch (error) {
console.error('Failed to create directories:', error);
}
const loadedScrapers = {};
@@ -35,9 +37,7 @@ function loadScrapers() {
.forEach((file) => {
const filePath = path.join(pluginsPath, file);
try {
const scraperModule = require(filePath);
const className = Object.keys(scraperModule)[0];
const ScraperClass = scraperModule[className];
@@ -45,8 +45,7 @@ function loadScrapers() {
typeof ScraperClass === 'function' &&
ScraperClass.prototype.fetchSearchResult
) {
const instance = new ScraperClass(fetchPath, cheerioPath);
const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser);
loadedScrapers[className] = {
instance: instance,
@@ -66,107 +65,27 @@ function loadScrapers() {
loadScrapers();
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();
}
});
function runDatabaseMigrations() {
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) {
console.error('Failed to get table info:', err.message);
return;
}
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) {
console.error('Failed to get table info:', err.message);
return;
}
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.');
}
});
});
}
const db = initDatabase(dbPath);
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
preload: path.join(__dirname, '/scripts/preload.js'),
preload: path.join(__dirname, '/src/preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile('views/index.html');
mainWindow.setMenu(null);
// mainWindow.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow();
initDiscordRPC();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
@@ -181,79 +100,12 @@ app.on('window-all-closed', function () {
}
});
ipcMain.handle('api:getSources', () => {
return Object.keys(loadedScrapers).map((name) => {
return {
name: name,
url: loadedScrapers[name].baseUrl,
};
});
});
const apiHandlers = require('./src/ipc/api-handlers')(loadedScrapers);
const dbHandlers = require('./src/ipc/db-handlers')(db);
ipcMain.handle('api: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 };
}
});
ipcMain.handle('db: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);
}
});
});
});
ipcMain.handle('api:getSources', apiHandlers.getSources);
ipcMain.handle('api:search', apiHandlers.search);
ipcMain.handle('db: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 });
}
}
);
});
});
ipcMain.handle('db: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 });
}
});
});
});
ipcMain.handle('db:getFavorites', dbHandlers.getFavorites);
ipcMain.handle('db:addFavorite', dbHandlers.addFavorite);
ipcMain.handle('db:removeFavorite', dbHandlers.removeFavorite);

135
package-lock.json generated
View File

@@ -1,15 +1,17 @@
{
"name": "waifu-board",
"version": "1.0.0",
"version": "v1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "waifu-board",
"version": "1.0.0",
"version": "v1.2.0",
"license": "ISC",
"dependencies": {
"@ryuziii/discord-rpc": "^1.0.1-rc.1",
"cheerio": "^1.1.2",
"dotenv": "^17.2.3",
"node-fetch": "^2.7.0",
"sqlite3": "^5.1.7"
},
@@ -575,6 +577,16 @@
"node": ">=14"
}
},
"node_modules/@ryuziii/discord-rpc": {
"version": "1.0.1-rc.1",
"resolved": "https://registry.npmjs.org/@ryuziii/discord-rpc/-/discord-rpc-1.0.1-rc.1.tgz",
"integrity": "sha512-q9YgU8Rj9To1LWzo4u8cXOHUorEkB5KZ5cdW80KoYtAUx+nQy7wYCEFiNh8kcmqFqQ8m3Fsx1IXx3UpVymkaSw==",
"license": "ISC",
"dependencies": {
"@types/ws": "^8.18.1",
"ws": "^8.18.3"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -672,7 +684,6 @@
"version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -708,6 +719,15 @@
"license": "MIT",
"optional": true
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -789,6 +809,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -899,7 +920,6 @@
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
@@ -919,7 +939,6 @@
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
@@ -942,7 +961,6 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -955,7 +973,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -977,7 +994,6 @@
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -991,7 +1007,6 @@
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -1007,8 +1022,7 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1",
@@ -1016,7 +1030,6 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -1616,7 +1629,6 @@
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
@@ -1646,9 +1658,9 @@
}
},
"node_modules/config-file-ts/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1723,7 +1735,6 @@
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
@@ -1737,7 +1748,6 @@
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
@@ -1979,6 +1989,7 @@
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"builder-util": "24.13.1",
@@ -2074,13 +2085,15 @@
}
},
"node_modules/dotenv": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
"integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==",
"dev": true,
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=10"
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
@@ -2211,16 +2224,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
@@ -3080,8 +3083,7 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/isbinaryfile": {
"version": "5.0.7",
@@ -3221,7 +3223,6 @@
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
@@ -3235,7 +3236,6 @@
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -3251,8 +3251,7 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1",
@@ -3260,7 +3259,6 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -3277,40 +3275,35 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
@@ -3962,7 +3955,6 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4275,8 +4267,7 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
@@ -4388,6 +4379,16 @@
"node": ">=12.0.0"
}
},
"node_modules/read-config-file/node_modules/dotenv": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
"integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=10"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -4408,7 +4409,6 @@
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
@@ -5125,7 +5125,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unique-filename": {
@@ -5319,6 +5318,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -5391,7 +5411,6 @@
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
@@ -5407,7 +5426,6 @@
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
@@ -5430,7 +5448,6 @@
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5443,7 +5460,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -5465,7 +5481,6 @@
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},

View File

@@ -1,6 +1,6 @@
{
"name": "waifu-board",
"version": "v1.2.0",
"version": "v1.3.0",
"description": "An image board app to store and browse your favorite waifus!",
"main": "main.js",
"scripts": {
@@ -19,7 +19,9 @@
"electron-builder": "^24.13.3"
},
"dependencies": {
"@ryuziii/discord-rpc": "^1.0.1-rc.1",
"cheerio": "^1.1.2",
"dotenv": "^17.2.3",
"node-fetch": "^2.7.0",
"sqlite3": "^5.1.7"
},

View File

@@ -1,585 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
  const browseButton = document.getElementById('browse-button');
  const favoritesButton = document.getElementById('favorites-button');
  const settingsButton = document.getElementById('settings-button');
  const browsePage = document.getElementById('browse-page');
  const pageTitle = document.getElementById('page-title');
  const headerContext = document.getElementById('header-context');
  const searchIconButton = document.getElementById('search-icon-button');
  const searchModal = document.getElementById('search-modal');
  const searchCloseButton = document.getElementById('search-close-button');
  const searchInput = document.getElementById('search-input');
  const searchButton = document.getElementById('search-button');
  const sourceList = document.getElementById('source-list');
  const contentGallery = document.getElementById('content-gallery');
  const favoritesGallery = document.getElementById('favorites-gallery');
  const loadingSpinner = document.getElementById('loading-spinner');
 
  const infiniteLoadingSpinner = document.getElementById(
    'infinite-loading-spinner'
  );
  const messageBar = document.getElementById('message-bar');
  const galleryPlaceholder = document.getElementById('gallery-placeholder');
  const layoutRadios = document.querySelectorAll('input[name="layout"]');
  const tagInfoModal = document.getElementById('tag-info-modal');
  const tagInfoCloseButton = document.getElementById(
    'tag-info-close-button'
  );
  const tagInfoContent = document.getElementById('tag-info-content');
  let currentFavorites = [];
  let currentSource = '';
  let currentQuery = '';
  let currentLayout = 'scroll';
  let currentPage = 1;
  let isLoading = false;
  let hasNextPage = true;
  async function populateSources() {
    console.log('Requesting sources from main process...');
    const sources = await window.api.getSources();
    sourceList.innerHTML = '';  
    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');
        currentSource = firstButton.dataset.source;
        updateHeader();
      }
    } else {
      console.warn('No sources were loaded from the main process.');
    }
  }
  sourceList.addEventListener('click', (e) => {
    const button = e.target.closest('.source-button');
    if (button) {
      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 (currentQuery) {
        performSearch();
      }
    }
  });
  function showPage(pageId) {
    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);
    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();
    } else if (pageId === 'settings-page') {
      activeTab = settingsButton;
      pageTitle.textContent = 'Settings';
      headerContext.textContent = '';
    }
    activeTab.classList.add('bg-indigo-600', 'text-white');
    activeTab.classList.remove('text-gray-400', 'hover:bg-gray-700');
  }
  browseButton.addEventListener('click', () => showPage('browse-page'));
  favoritesButton.addEventListener('click', () => showPage('favorites-page'));
  settingsButton.addEventListener('click', () => showPage('settings-page'));
  searchIconButton.addEventListener('click', () => {
    searchModal.classList.remove('hidden');
    searchInput.focus();
    searchInput.select();
  });
  searchCloseButton.addEventListener('click', () => {
    searchModal.classList.add('hidden');
  });
  searchButton.addEventListener('click', () => {
    performSearch();
  });
 
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      searchModal.classList.add('hidden');
    }
  });
  tagInfoCloseButton.addEventListener('click', () => {
    tagInfoModal.classList.add('hidden');
  });
 
  tagInfoModal.addEventListener('click', (e) => {
    if (e.target === tagInfoModal) {
      tagInfoModal.classList.add('hidden');
    }
  });
  function showTagModal(tags) {
    tagInfoContent.innerHTML = '';
    if (!tags || tags.length === 0) {
      tagInfoContent.innerHTML =
        '<p class="text-gray-400">No tags available for this image.</p>';
      tagInfoModal.classList.remove('hidden');
      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);
      }
    });
    tagInfoContent.appendChild(fragment);
    tagInfoModal.classList.remove('hidden');
  }
  function updateHeader() {
    if (currentSource) {
      headerContext.textContent = `Source: ${currentSource}`;
    } else {
      headerContext.textContent = 'No source selected';
    }
  }
  async function performSearch() {
    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');
    loadMoreResults();
  }
  async function loadMoreResults() {
    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;
  }
  browsePage.addEventListener('scroll', () => {
    if (
      browsePage.scrollTop + browsePage.clientHeight >=
      browsePage.scrollHeight - 600
    ) {
      loadMoreResults();
    }
  });
  async function loadFavorites() {
    applyLayoutToGallery(favoritesGallery, currentLayout);
    favoritesGallery.innerHTML =
      '<div class="text-center p-10"><p class="text-gray-400">Loading favorites...</p></div>';
    currentFavorites = await window.api.getFavorites();
    if (currentFavorites.length === 0) {
      applyLayoutToGallery(favoritesGallery, currentLayout);
      favoritesGallery.innerHTML =
        '<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
      return;
    }
    applyLayoutToGallery(favoritesGallery, currentLayout);
    favoritesGallery.innerHTML = '';
    const fragment = document.createDocumentFragment();
    currentFavorites.forEach((fav) => {
      const card = createImageCard(
        fav.id,
        fav.tags ? fav.tags.split(',') : [],
        fav.image_url,
        fav.thumbnail_url,
        'fav'
      );
      fragment.appendChild(card);
    });
    favoritesGallery.appendChild(fragment);
  }
  async function handleAddFavorite(id, tags, imageUrl, thumbnailUrl) {
    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) {
      showMessage('Added to favorites!', 'success');
    } else {
      showMessage(result.error, 'error');
    }
  }
  async function handleRemoveFavorite(id) {
    const result = await window.api.removeFavorite(id);
    if (result.success) {
      showMessage('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) {
            applyLayoutToGallery(favoritesGallery, currentLayout);
            favoritesGallery.innerHTML =
              '<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
          }
        }, 300);
      }
    } else {
      showMessage(result.error, 'error');
    }
  }
  function createImageCard(id, tags, imageUrl, thumbnailUrl, type) {
    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));
    if (type === 'browse') {
      buttonContainer.appendChild(
        createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl)
      );
    } else {
      buttonContainer.appendChild(createRemoveFavoriteButton(id));
    }
    imageContainer.appendChild(buttonContainer);
    return entry;
  }
  // ------------------------------------------------------------------
  // DELETED: The source-specific getFullImageUrl function is removed.
  // The extensions must now provide the correct URLs in the search result.
  // ------------------------------------------------------------------
  function createInfoButton(safeTags) {
    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();
      showTagModal(safeTags);
    };
    return button;
  }
  function createAddFavoriteButton(id, safeTags, imageUrl, thumbnailUrl) {
    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);
    };
    return button;
  }
  function createRemoveFavoriteButton(id) {
    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);
    };
    return button;
  }
  function showMessage(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);
  }
  function loadSettings() {
    const savedLayout = localStorage.getItem('waifuBoardLayout') || 'scroll';
    currentLayout = savedLayout;
    const savedRadio = document.querySelector(
      `input[name="layout"][value="${savedLayout}"]`
    );
    if (savedRadio) {
      savedRadio.checked = true;
    } else {
      document.getElementById('layout-scroll').checked = true;
      currentLayout = 'scroll';
      localStorage.setItem('waifuBoardLayout', 'scroll');
    }
  }
  function handleLayoutChange(e) {
    const newLayout = e.target.value;
    localStorage.setItem('waifuBoardLayout', newLayout);
    currentLayout = newLayout;
    console.log('Layout changed to:', newLayout);
    if (browsePage.classList.contains('hidden')) {
      loadFavorites();
    } else {
      if (currentQuery) {
        performSearch();
      } else {
        applyLayoutToGallery(contentGallery, currentLayout);
      }
    }
  }
  function applyLayoutToGallery(galleryElement, layout) {
    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');
    }
  }
  layoutRadios.forEach((radio) => {
    radio.addEventListener('change', handleLayoutChange);
  });
  loadSettings();
  populateSources();
  showPage('browse-page');
});

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);
}

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');
});

View File

@@ -1,6 +1,7 @@
const GITHUB_OWNER = 'ItsSkaiya';
const GITHUB_REPO = 'WaifuBoard';
const CURRENT_VERSION = 'v1.2.0';
const CURRENT_VERSION = 'v1.3.0';
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
let currentVersionDisplay;
let latestVersionDisplay;
@@ -17,6 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
checkForUpdates();
setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL);
});
function showToast(latestVersion) {
@@ -31,6 +34,13 @@ function showToast(latestVersion) {
}
}
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);

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');
}
});
}

View File

@@ -317,8 +317,8 @@
<p>An update is required for Waifu Board! newest version - <span id="latestVersionDisplay"></span></p>
</div>
<script src="../scripts/renderer.js"></script>
<script src="../scripts/updateNotification.js"></script>
<script type="module" src="../src/renderer.js"></script>
<script src="../src/updateNotification.js"></script>
</body>
</html>