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) => ` +
+
+ +
+ ${typeLabel} +
+ +
+

${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 = '

No extensions installed.

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

No extensions installed.

'; + 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 @@