From 5f3020ca6e89a6254d801fc4008d9f79af9a6f5d Mon Sep 17 00:00:00 2001 From: itsskaiya Date: Wed, 19 Nov 2025 16:28:33 -0500 Subject: [PATCH] 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 --- .gitignore | 3 +- main.js | 182 +------- package-lock.json | 135 +++--- package.json | 4 +- scripts/renderer.js | 585 ------------------------- src/content/image-handler.js | 115 +++++ src/database/db-init.js | 74 ++++ src/discord-rpc.js | 82 ++++ src/extensions/load-extensions.js | 56 +++ src/favorites/favorites-handler.js | 76 ++++ src/favorites/favorites-loader.js | 33 ++ src/ipc/api-handlers.js | 29 ++ src/ipc/db-handlers.js | 53 +++ src/modules/layout-manager.js | 32 ++ src/modules/navigation-handler.js | 52 +++ src/modules/search-handler.js | 115 +++++ src/modules/ui-utils.js | 18 + {scripts => src}/preload.js | 0 src/renderer.js | 132 ++++++ {scripts => src}/updateNotification.js | 12 +- src/utils/dom-loader.js | 26 ++ src/utils/headless-browser.js | 67 +++ src/utils/keybinds.js | 7 + views/index.html | 4 +- 24 files changed, 1077 insertions(+), 815 deletions(-) delete mode 100644 scripts/renderer.js create mode 100644 src/content/image-handler.js create mode 100644 src/database/db-init.js create mode 100644 src/discord-rpc.js create mode 100644 src/extensions/load-extensions.js create mode 100644 src/favorites/favorites-handler.js create mode 100644 src/favorites/favorites-loader.js create mode 100644 src/ipc/api-handlers.js create mode 100644 src/ipc/db-handlers.js create mode 100644 src/modules/layout-manager.js create mode 100644 src/modules/navigation-handler.js create mode 100644 src/modules/search-handler.js create mode 100644 src/modules/ui-utils.js rename {scripts => src}/preload.js (100%) create mode 100644 src/renderer.js rename {scripts => src}/updateNotification.js (88%) create mode 100644 src/utils/dom-loader.js create mode 100644 src/utils/headless-browser.js create mode 100644 src/utils/keybinds.js diff --git a/.gitignore b/.gitignore index b2d59d1..c7fb7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules -/dist \ No newline at end of file +/dist +.env \ No newline at end of file diff --git a/main.js b/main.js index 35752be..b23f377 100644 --- a/main.js +++ b/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 }); - } - }); - }); -}); \ No newline at end of file +ipcMain.handle('db:getFavorites', dbHandlers.getFavorites); +ipcMain.handle('db:addFavorite', dbHandlers.addFavorite); +ipcMain.handle('db:removeFavorite', dbHandlers.removeFavorite); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 08485b9..d680a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index a3f894d..d1f27df 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/scripts/renderer.js b/scripts/renderer.js deleted file mode 100644 index 8acc568..0000000 --- a/scripts/renderer.js +++ /dev/null @@ -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 = `${source.name.substring( -            0, -            2 -          )}`; -          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 = -        '

No tags available for this image.

'; -      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 = -          '

No results found. Please try another search term.

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

Found results, but none had valid images.

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

Loading favorites...

'; -    currentFavorites = await window.api.getFavorites(); - -    if (currentFavorites.length === 0) { -      applyLayoutToGallery(favoritesGallery, currentLayout); -      favoritesGallery.innerHTML = -        '

You haven\'t saved any favorites yet.

'; -      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 = -              '

You haven\'t saved any favorites yet.

'; -          } -        }, 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 = ` -        -      `; -    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 = ` -        -      `; -    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 = ` -        -      `; -    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'); -}); \ No newline at end of file diff --git a/src/content/image-handler.js b/src/content/image-handler.js new file mode 100644 index 0000000..a81e819 --- /dev/null +++ b/src/content/image-handler.js @@ -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 = + '

No tags available for this image.

'; + 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 = ` + + `; + 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; +} \ No newline at end of file diff --git a/src/database/db-init.js b/src/database/db-init.js new file mode 100644 index 0000000..7228d42 --- /dev/null +++ b/src/database/db-init.js @@ -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; \ No newline at end of file diff --git a/src/discord-rpc.js b/src/discord-rpc.js new file mode 100644 index 0000000..f95b7eb --- /dev/null +++ b/src/discord-rpc.js @@ -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 }; \ No newline at end of file diff --git a/src/extensions/load-extensions.js b/src/extensions/load-extensions.js new file mode 100644 index 0000000..91d354d --- /dev/null +++ b/src/extensions/load-extensions.js @@ -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 = `${source.name.substring( + 0, + 2 + )}`; + 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; +} \ No newline at end of file diff --git a/src/favorites/favorites-handler.js b/src/favorites/favorites-handler.js new file mode 100644 index 0000000..43c3aee --- /dev/null +++ b/src/favorites/favorites-handler.js @@ -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 = + '

You haven\'t saved any favorites yet.

'; + } + }, 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 = ` + + `; + + 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 = ` + + `; + + button.onclick = (e) => { + e.stopPropagation(); + handleRemoveFavorite(id, favoritesGallery, showMessageCallback, applyLayoutCallback, currentLayout); + }; + return button; +} \ No newline at end of file diff --git a/src/favorites/favorites-loader.js b/src/favorites/favorites-loader.js new file mode 100644 index 0000000..dfccf6b --- /dev/null +++ b/src/favorites/favorites-loader.js @@ -0,0 +1,33 @@ +export async function loadFavorites(favoritesGallery, currentLayout, applyLayoutCallback, createImageCardCallback) { + + applyLayoutCallback(favoritesGallery, currentLayout); + favoritesGallery.innerHTML = + '

Loading favorites...

'; + + const currentFavorites = await window.api.getFavorites(); + + if (currentFavorites.length === 0) { + applyLayoutCallback(favoritesGallery, currentLayout); + favoritesGallery.innerHTML = + '

You haven\'t saved any favorites yet.

'; + 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); +} \ No newline at end of file diff --git a/src/ipc/api-handlers.js b/src/ipc/api-handlers.js new file mode 100644 index 0000000..28f93f9 --- /dev/null +++ b/src/ipc/api-handlers.js @@ -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 }; + } + }, + }; +}; \ No newline at end of file diff --git a/src/ipc/db-handlers.js b/src/ipc/db-handlers.js new file mode 100644 index 0000000..637b87d --- /dev/null +++ b/src/ipc/db-handlers.js @@ -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 }); + } + }); + }); + }, + }; +}; \ No newline at end of file diff --git a/src/modules/layout-manager.js b/src/modules/layout-manager.js new file mode 100644 index 0000000..94ca046 --- /dev/null +++ b/src/modules/layout-manager.js @@ -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); +} \ No newline at end of file diff --git a/src/modules/navigation-handler.js b/src/modules/navigation-handler.js new file mode 100644 index 0000000..e8a6828 --- /dev/null +++ b/src/modules/navigation-handler.js @@ -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'); + } +} \ No newline at end of file diff --git a/src/modules/search-handler.js b/src/modules/search-handler.js new file mode 100644 index 0000000..6db3edd --- /dev/null +++ b/src/modules/search-handler.js @@ -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 = + '

No results found. Please try another search term.

'; + } + 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 = + '

Found results, but none had valid images.

'; + } + 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; +} \ No newline at end of file diff --git a/src/modules/ui-utils.js b/src/modules/ui-utils.js new file mode 100644 index 0000000..9d189b1 --- /dev/null +++ b/src/modules/ui-utils.js @@ -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); +} \ No newline at end of file diff --git a/scripts/preload.js b/src/preload.js similarity index 100% rename from scripts/preload.js rename to src/preload.js diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..37a16a9 --- /dev/null +++ b/src/renderer.js @@ -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'); +}); \ No newline at end of file diff --git a/scripts/updateNotification.js b/src/updateNotification.js similarity index 88% rename from scripts/updateNotification.js rename to src/updateNotification.js index 8036341..801830d 100644 --- a/scripts/updateNotification.js +++ b/src/updateNotification.js @@ -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); diff --git a/src/utils/dom-loader.js b/src/utils/dom-loader.js new file mode 100644 index 0000000..bf4190c --- /dev/null +++ b/src/utils/dom-loader.js @@ -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'), + }; +} \ No newline at end of file diff --git a/src/utils/headless-browser.js b/src/utils/headless-browser.js new file mode 100644 index 0000000..29e7725 --- /dev/null +++ b/src/utils/headless-browser.js @@ -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(); \ No newline at end of file diff --git a/src/utils/keybinds.js b/src/utils/keybinds.js new file mode 100644 index 0000000..e6b4d8d --- /dev/null +++ b/src/utils/keybinds.js @@ -0,0 +1,7 @@ +export function setupGlobalKeybinds(searchModal) { + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + searchModal.classList.add('hidden'); + } + }); +} \ No newline at end of file diff --git a/views/index.html b/views/index.html index 21ceb8e..959aaf9 100644 --- a/views/index.html +++ b/views/index.html @@ -317,8 +317,8 @@

An update is required for Waifu Board! newest version -

- - + + \ No newline at end of file