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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/dist
|
||||
.env
|
||||
182
main.js
182
main.js
@@ -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
135
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
115
src/content/image-handler.js
Normal file
115
src/content/image-handler.js
Normal 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
74
src/database/db-init.js
Normal 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
82
src/discord-rpc.js
Normal 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 };
|
||||
56
src/extensions/load-extensions.js
Normal file
56
src/extensions/load-extensions.js
Normal 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;
|
||||
}
|
||||
76
src/favorites/favorites-handler.js
Normal file
76
src/favorites/favorites-handler.js
Normal 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;
|
||||
}
|
||||
33
src/favorites/favorites-loader.js
Normal file
33
src/favorites/favorites-loader.js
Normal 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
29
src/ipc/api-handlers.js
Normal 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
53
src/ipc/db-handlers.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
32
src/modules/layout-manager.js
Normal file
32
src/modules/layout-manager.js
Normal 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);
|
||||
}
|
||||
52
src/modules/navigation-handler.js
Normal file
52
src/modules/navigation-handler.js
Normal 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');
|
||||
}
|
||||
}
|
||||
115
src/modules/search-handler.js
Normal file
115
src/modules/search-handler.js
Normal 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
18
src/modules/ui-utils.js
Normal 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
132
src/renderer.js
Normal 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');
|
||||
});
|
||||
@@ -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
26
src/utils/dom-loader.js
Normal 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'),
|
||||
};
|
||||
}
|
||||
67
src/utils/headless-browser.js
Normal file
67
src/utils/headless-browser.js
Normal 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
7
src/utils/keybinds.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function setupGlobalKeybinds(searchModal) {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
searchModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user