diff --git a/main.js b/main.js
index 9cee976..ccb3f43 100644
--- a/main.js
+++ b/main.js
@@ -102,6 +102,15 @@ ipcMain.handle('db:getFavorites', dbHandlers.getFavorites);
ipcMain.handle('db:addFavorite', dbHandlers.addFavorite);
ipcMain.handle('db:removeFavorite', dbHandlers.removeFavorite);
+ipcMain.handle('api:getInstalledExtensions', apiHandlers.getInstalledExtensions);
+ipcMain.handle('api:installExtension', apiHandlers.installExtension);
+ipcMain.handle('api:uninstallExtension', apiHandlers.uninstallExtension);
+
+ipcMain.on('app:restart', () => {
+ app.relaunch();
+ app.exit();
+});
+
ipcMain.on('toggle-dev-tools', (event) => {
event.sender.toggleDevTools();
});
\ No newline at end of file
diff --git a/package.json b/package.json
index 63d9f75..2afed21 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "waifu-board",
- "version": "v1.6.1",
+ "version": "v1.6.2",
"description": "An image board app to store and browse your favorite waifus!",
"main": "main.js",
"scripts": {
diff --git a/src/ipc/api-handlers.js b/src/ipc/api-handlers.js
index a27c601..d71a435 100644
--- a/src/ipc/api-handlers.js
+++ b/src/ipc/api-handlers.js
@@ -1,7 +1,9 @@
const fs = require('fs');
+const path = require('path');
const fetchPath = require.resolve('node-fetch');
const cheerioPath = require.resolve('cheerio');
const fetch = require(fetchPath);
+const { app } = require('electron');
function peekProperty(filePath, propertyName) {
try {
@@ -80,17 +82,8 @@ module.exports = function (availableScrapers, headlessBrowser) {
try {
const instance = getScraperInstance(source);
if (!instance.findChapters) throw new Error("Extension does not support chapters.");
-
- const result = await instance.findChapters(mangaId);
-
- if (Array.isArray(result)) {
- return { success: true, data: result };
- } else if (result && result.chapters) {
- return { success: true, data: result.chapters, extra: { cover: result.cover } };
- }
-
- return { success: true, data: [] };
-
+ const chapters = await instance.findChapters(mangaId);
+ return { success: true, data: chapters };
} catch (err) {
console.error(`Error fetching chapters from ${source}:`, err);
return { success: false, error: err.message };
@@ -147,6 +140,49 @@ module.exports = function (availableScrapers, headlessBrowser) {
} catch (err) {
return { success: false, error: err.message };
}
+ },
+
+
+ getInstalledExtensions: async () => {
+ const pPath = path.join(app.getPath('home'), 'WaifuBoards', 'extensions');
+ try {
+ return fs.readdirSync(pPath).filter(f => f.endsWith('.js'));
+ } catch (e) {
+ console.error("Error reading extensions dir:", e);
+ return [];
+ }
+ },
+
+ installExtension: async (event, filename, url) => {
+ const https = require('https'); // Require https here or at top
+ const pPath = path.join(app.getPath('home'), 'WaifuBoards', 'extensions', filename);
+
+ return new Promise((resolve) => {
+ const file = fs.createWriteStream(pPath);
+ https.get(url, function(response) {
+ response.pipe(file);
+ file.on('finish', function() {
+ file.close(() => resolve(true));
+ });
+ }).on('error', function(err) {
+ fs.unlink(pPath, () => {});
+ resolve(false);
+ });
+ });
+ },
+
+ uninstallExtension: async (event, filename) => {
+ const pPath = path.join(app.getPath('home'), 'WaifuBoards', 'extensions', filename);
+ try {
+ if (fs.existsSync(pPath)) {
+ fs.unlinkSync(pPath);
+ return true;
+ }
+ return false;
+ } catch (e) {
+ console.error("Uninstall error:", e);
+ return false;
+ }
}
};
};
\ No newline at end of file
diff --git a/src/marketplace.js b/src/marketplace.js
new file mode 100644
index 0000000..fe61112
--- /dev/null
+++ b/src/marketplace.js
@@ -0,0 +1,298 @@
+const REPO_OWNER = 'ItsSkaiya';
+const REPO_NAME = 'WaifuBoard-Extensions';
+const API_URL = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/extensions?ref=main`;
+
+document.addEventListener('DOMContentLoaded', async () => {
+ const browseGrid = document.getElementById('marketplace-grid');
+ const installedGrid = document.getElementById('installed-grid');
+
+ const statTotal = document.getElementById('stat-total');
+ const statInstalled = document.getElementById('stat-installed');
+
+ const tabBrowse = document.getElementById('tab-browse');
+ const tabInstalled = document.getElementById('tab-installed');
+ const viewBrowse = document.getElementById('view-browse');
+ const viewInstalled = document.getElementById('view-installed');
+
+ let allRemoteExtensions = [];
+ let installedExtensionsList = [];
+
+ const messageBar = document.getElementById('message-bar');
+
+ function showMessage(msg, type = 'success') {
+ if (!messageBar) return;
+ messageBar.textContent = msg;
+ messageBar.className = `toast ${type === 'error' ? 'error' : ''}`;
+ messageBar.classList.remove('hidden');
+ setTimeout(() => messageBar.classList.add('hidden'), 3000);
+ }
+
+ function updateStats() {
+ if(statTotal) statTotal.textContent = allRemoteExtensions.length;
+ if(statInstalled) statInstalled.textContent = installedExtensionsList.length;
+ }
+
+ function showRestartToast() {
+ if (document.getElementById('restart-toast')) return;
+
+ const toast = document.createElement('div');
+ toast.id = 'restart-toast';
+ toast.style.cssText = `
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: #1f2937;
+ border: 1px solid var(--accent);
+ border-radius: 12px;
+ padding: 1rem 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
+ z-index: 2000;
+ animation: slideUp 0.3s ease-out;
+ `;
+
+ toast.innerHTML = `
+
+ Restart Required
+ Changes will apply after restart.
+
+
+ `;
+
+ document.body.appendChild(toast);
+
+ const btn = document.getElementById('btn-restart-now');
+ btn.onmouseover = () => btn.style.opacity = '0.9';
+ btn.onmouseout = () => btn.style.opacity = '1';
+
+ btn.onclick = () => {
+ if (window.api && typeof window.api.restartApp === 'function') {
+ window.api.restartApp();
+ } else {
+ console.error("Restart API not found in window.api");
+ alert("Please close and reopen the application manually.");
+ }
+ };
+ }
+
+ function switchTab(tab) {
+ if (tab === 'browse') {
+ tabBrowse.classList.add('active');
+ tabInstalled.classList.remove('active');
+ viewBrowse.classList.remove('hidden');
+ viewInstalled.classList.add('hidden');
+ } else {
+ tabBrowse.classList.remove('active');
+ tabInstalled.classList.add('active');
+ viewBrowse.classList.add('hidden');
+ viewInstalled.classList.remove('hidden');
+ renderInstalledGrid();
+ }
+ }
+
+ if(tabBrowse) tabBrowse.onclick = () => switchTab('browse');
+ if(tabInstalled) tabInstalled.onclick = () => switchTab('installed');
+
+ async function fetchInstalledExtensions() {
+ try {
+ const extensions = await window.api.getInstalledExtensions();
+ installedExtensionsList = extensions || [];
+ } catch (e) {
+ console.error("Failed to fetch installed extensions:", e);
+ installedExtensionsList = [];
+ }
+ }
+
+ async function fetchRemoteExtensions() {
+ try {
+ const res = await fetch(API_URL);
+ if (!res.ok) throw new Error(`GitHub API Error: ${res.status}`);
+ const data = await res.json();
+
+ allRemoteExtensions = data.filter(item => item.name.endsWith('.js'));
+ renderBrowseGrid(allRemoteExtensions);
+ updateStats();
+ } catch (e) {
+ if(browseGrid) browseGrid.innerHTML = `Failed to load marketplace.
${e.message}
`;
+ }
+ }
+
+ async function getExtensionDetails(url) {
+ try {
+ const res = await fetch(url);
+ const text = await res.text();
+
+ const baseUrlMatch = text.match(/baseUrl\s*=\s*["']([^"']+)["']/);
+ let baseUrl = baseUrlMatch ? baseUrlMatch[1] : null;
+
+ if (baseUrl) {
+ try {
+ const urlObj = new URL(baseUrl);
+ baseUrl = urlObj.hostname;
+ } catch(e) {
+ console.warn("Invalid URL in extension:", baseUrl);
+ }
+ }
+
+ const classMatch = text.match(/class\s+(\w+)/);
+ const name = classMatch ? classMatch[1] : null;
+
+ let type = 'Image';
+ if (text.includes('type = "book-board"') || text.includes("type = 'book-board'")) {
+ type = 'Book';
+ }
+
+ return { baseUrl, name, type };
+ } catch (e) {
+ return { baseUrl: null, name: null, type: 'Unknown' };
+ }
+ }
+
+ async function createCard(item, isLocalOnly = false) {
+ const isInstalled = installedExtensionsList.includes(item.name);
+
+ const downloadUrl = item.download_url || null;
+ let sizeKB = item.size ? (item.size / 1024).toFixed(1) + ' KB' : 'Local';
+
+ let iconUrl = '';
+ let displayName = item.name.replace('.js', '');
+ let typeLabel = 'Extension';
+ let typeClass = 'type-image';
+
+ if (downloadUrl) {
+ const details = await getExtensionDetails(downloadUrl);
+ displayName = details.name || displayName;
+ const domain = details.baseUrl || 'github.com';
+ iconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`;
+
+ if (details.type === 'Book') {
+ typeLabel = 'Book Board';
+ typeClass = 'type-book';
+ } else {
+ typeLabel = 'Image Board';
+ typeClass = 'type-image';
+ }
+ } else {
+ iconUrl = `https://ui-avatars.com/api/?name=${displayName}&background=18181b&color=fff&length=1`;
+ }
+
+ const card = document.createElement('div');
+ card.className = 'extension-card';
+ card.dataset.name = item.name;
+
+ const downloadIcon = '';
+ const trashIcon = '';
+ const checkIcon = '';
+
+ const renderInner = (installed) => `
+
+
+
+
${displayName}
+
${item.name}
+
+
+
+ `;
+
+ card.innerHTML = renderInner(isInstalled);
+
+ card.addEventListener('click', async (e) => {
+ const btn = e.target.closest('.install-btn');
+ if (!btn) return;
+
+ const currentlyInstalled = installedExtensionsList.includes(item.name);
+
+ if (currentlyInstalled) {
+ const success = await window.api.uninstallExtension(item.name);
+ if (success) {
+ installedExtensionsList = installedExtensionsList.filter(n => n !== item.name);
+ card.innerHTML = renderInner(false);
+ updateStats();
+
+ if (tabInstalled.classList.contains('active')) {
+ card.remove();
+ if (installedGrid.children.length === 0) {
+ installedGrid.innerHTML = '';
+ }
+ }
+ showRestartToast();
+ } else {
+ showMessage('Failed to uninstall', 'error');
+ }
+ } else {
+ if (!downloadUrl) return;
+ const originalHTML = btn.innerHTML;
+ btn.innerHTML = '';
+
+ const success = await window.api.installExtension(item.name, downloadUrl);
+ if (success) {
+ installedExtensionsList.push(item.name);
+ card.innerHTML = renderInner(true);
+ updateStats();
+ showMessage(`Installed ${displayName}`);
+ showRestartToast();
+ } else {
+ showMessage('Failed to install', 'error');
+ btn.innerHTML = originalHTML;
+ }
+ }
+ });
+
+ return card;
+ }
+
+ async function renderBrowseGrid(list) {
+ browseGrid.innerHTML = '';
+ if (list.length === 0) {
+ browseGrid.innerHTML = 'No extensions found.
';
+ return;
+ }
+ for (const item of list) {
+ const card = await createCard(item);
+ browseGrid.appendChild(card);
+ }
+ }
+
+ async function renderInstalledGrid() {
+ installedGrid.innerHTML = '';
+ if (installedExtensionsList.length === 0) {
+ installedGrid.innerHTML = '';
+ return;
+ }
+
+ for (const filename of installedExtensionsList) {
+ const remoteData = allRemoteExtensions.find(r => r.name === filename);
+ const item = remoteData || { name: filename, download_url: null, size: 0 };
+ const card = await createCard(item, !remoteData);
+ installedGrid.appendChild(card);
+ }
+ }
+
+ await fetchInstalledExtensions();
+ await fetchRemoteExtensions();
+});
\ No newline at end of file
diff --git a/src/preload.js b/src/preload.js
index 8b356b4..7151787 100644
--- a/src/preload.js
+++ b/src/preload.js
@@ -13,4 +13,10 @@ contextBridge.exposeInMainWorld('api', {
toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'),
getSources: () => ipcRenderer.invoke('api:getSources'),
+
+ getInstalledExtensions: () => ipcRenderer.invoke('api:getInstalledExtensions'),
+ installExtension: (name, url) => ipcRenderer.invoke('api:installExtension', name, url),
+ uninstallExtension: (name) => ipcRenderer.invoke('api:uninstallExtension', name),
+
+ restartApp: () => ipcRenderer.send('app:restart')
});
\ No newline at end of file
diff --git a/src/updateNotification.js b/src/updateNotification.js
index 1232f38..78f31c9 100644
--- a/src/updateNotification.js
+++ b/src/updateNotification.js
@@ -1,6 +1,6 @@
const GITHUB_OWNER = 'ItsSkaiya';
const GITHUB_REPO = 'WaifuBoard';
-const CURRENT_VERSION = 'v1.6.1';
+const CURRENT_VERSION = 'v1.6.2';
const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000;
let currentVersionDisplay;
diff --git a/views/books.html b/views/books.html
index 20a3375..f69caf7 100644
--- a/views/books.html
+++ b/views/books.html
@@ -40,6 +40,10 @@