diff --git a/package.json b/package.json index 2afed21..902ef81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waifu-board", - "version": "v1.6.2", + "version": "v1.6.3", "description": "An image board app to store and browse your favorite waifus!", "main": "main.js", "scripts": { diff --git a/src/emulator/emulator.js b/src/emulator/emulator.js new file mode 100644 index 0000000..b42e986 --- /dev/null +++ b/src/emulator/emulator.js @@ -0,0 +1,306 @@ +const CheerioShim = { + load: (html) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + const $ = (selector, context) => { + let elements; + if (selector && selector.nodeType) { + elements = [selector]; + } else if (Array.isArray(selector)) { + elements = selector; + } else { + const root = (context && context[0]) ? context[0] : (context || doc); + elements = Array.from(root.querySelectorAll ? root.querySelectorAll(selector) : []); + } + return createWrapper(elements); + }; + + function createWrapper(elements) { + elements.forEach((el, i) => elements[i] = el); + + elements.attr = (name) => { + if (elements.length === 0) return undefined; + return elements[0].getAttribute ? elements[0].getAttribute(name) : null; + }; + + elements.text = () => { + return elements.map(el => el.textContent).join(''); + }; + + elements.find = (selector) => { + let found = []; + elements.forEach(el => { + if (el.querySelectorAll) { + found = found.concat(Array.from(el.querySelectorAll(selector))); + } + }); + return createWrapper(found); + }; + + elements.each = (callback) => { + elements.forEach((el, i) => { + callback(i, el); + }); + return elements; + }; + + elements.map = (callback) => { + const results = elements.map((el, i) => callback(i, el)); + return createWrapper(results.filter(r => r !== null && r !== undefined)); + }; + + elements.get = () => Array.from(elements); + + elements.filter = (selectorOrFn) => { + if (typeof selectorOrFn === 'function') { + return createWrapper(elements.filter(selectorOrFn)); + } + return createWrapper(elements.filter(el => el.matches && el.matches(selectorOrFn))); + }; + + elements.first = () => createWrapper(elements.length ? [elements[0]] : []); + elements.last = () => createWrapper(elements.length ? [elements[elements.length - 1]] : []); + elements.eq = (i) => createWrapper(elements.length > i ? [elements[i]] : []); + + return elements; + } + + return $; + } +}; + +window.require = (moduleName) => { + if (moduleName === 'cheerio') return CheerioShim; + + if (moduleName === 'node-fetch' || moduleName === 'fetch') return window.fetch.bind(window); + + return {}; +}; + +window.module = { exports: {} }; + +class MockBrowser { + constructor() { + } + + async scrape(url, pageFn, options = {}) { + emulatorLog(`[Browser] Scraping: ${url}`, 'info'); + + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const html = await response.text(); + + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + + return await this.executePageFn(pageFn, doc); + + } catch (error) { + emulatorLog(`[Browser] Error: ${error.message}`, 'error'); + if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { + emulatorLog('Tip: This looks like a CORS error. Ensure you have a "Allow CORS" extension enabled in your browser for testing.', 'warn'); + } + throw error; + } + } + + async executePageFn(pageFn, virtualDoc) { + const fnString = pageFn.toString(); + const wrapper = new Function("document", `return (${fnString})();`); + return wrapper(virtualDoc); + } +} + +const codeInput = document.getElementById('code-input'); +const runBtn = document.getElementById('run-btn'); +const outputVisual = document.getElementById('visual-container'); +const outputJson = document.getElementById('json-content'); +const outputConsole = document.getElementById('console-content'); + +const originalConsoleLog = console.log; +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; + +function captureLogs() { + console.log = (...args) => { + originalConsoleLog(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'info'); + }; + console.error = (...args) => { + originalConsoleError(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'error'); + }; + console.warn = (...args) => { + originalConsoleWarn(...args); + emulatorLog(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '), 'warn'); + }; +} + +function emulatorLog(msg, type = 'info') { + const div = document.createElement('div'); + div.className = `log-entry log-${type}`; + div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + outputConsole.appendChild(div); + outputConsole.scrollTop = outputConsole.scrollHeight; +} + +window.switchTab = (tabName) => { + ['visual', 'json', 'console'].forEach(t => { + document.getElementById(`output-${t}`).classList.add('hidden'); + }); + document.getElementById(`output-${tabName}`).classList.remove('hidden'); + + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); + if (event && event.target) event.target.classList.add('active'); +}; + +runBtn.addEventListener('click', async () => { + outputVisual.innerHTML = ''; + outputJson.textContent = ''; + outputConsole.innerHTML = ''; + window.module.exports = {}; + captureLogs(); + + const code = codeInput.value; + const functionName = document.getElementById('func-select').value; + const argInput = document.getElementById('arg-input').value; + const pageInput = parseInt(document.getElementById('page-input').value) || 1; + + if (!code.trim()) { + alert("Please paste extension code first."); + return; + } + + emulatorLog("--- Starting Execution ---"); + + try { + try { + new Function(code)(); + } catch (e) { + throw new Error(`Syntax/Runtime Error in Extension Code: ${e.message}`); + } + + const exports = window.module.exports; + const keys = Object.keys(exports); + if (keys.length === 0) throw new Error("No class found in module.exports. Did you add 'module.exports = { ClassName };'?"); + + const ExtensionClass = exports[keys[0]]; + emulatorLog(`Loaded Class: ${keys[0]}`, 'info'); + + const browserInstance = new MockBrowser(); + const extension = new ExtensionClass('node-fetch', 'cheerio', browserInstance); + + if (typeof extension[functionName] !== 'function') { + throw new Error(`Function '${functionName}' not found in class '${keys[0]}'.`); + } + + emulatorLog(`Calling ${functionName}('${argInput}', ${pageInput})...`); + + document.querySelector('.loading-state').classList.remove('hidden'); + + let result; + if (functionName === 'fetchSearchResult') { + result = await extension.fetchSearchResult(argInput, pageInput); + } else if (functionName === 'fetchInfo') { + result = await extension.fetchInfo(argInput); + } else if (functionName === 'findChapters') { + result = await extension.findChapters(argInput); + } else if (functionName === 'findChapterPages') { + result = await extension.findChapterPages(argInput); + } + + document.querySelector('.loading-state').classList.add('hidden'); + + emulatorLog("Data received. Rendering...", 'info'); + renderJson(result); + renderVisuals(result, functionName); + + if (result) switchTab('visual'); + + } catch (err) { + document.querySelector('.loading-state').classList.add('hidden'); + emulatorLog(err.message, 'error'); + console.error(err); + } +}); + +function renderJson(data) { + outputJson.textContent = JSON.stringify(data, null, 2); +} + +function renderVisuals(data, type) { + outputVisual.innerHTML = ''; + + if (!data) { + outputVisual.innerHTML = '
No data returned (null/undefined). Check Console for errors.
'; + return; + } + + if (type === 'fetchSearchResult') { + if (data.results && Array.isArray(data.results) && data.results.length > 0) { + data.results.forEach(item => { + const card = document.createElement('div'); + card.className = 'visual-card'; + + const imgSrc = item.image || item.cover || item.sampleImageUrl || ''; + + card.innerHTML = ` + +
+ ${item.title || ('ID: ' + item.id)} +
+ `; + outputVisual.appendChild(card); + }); + } else { + outputVisual.innerHTML = '
No results found in "results" array.
'; + } + } + else if (type === 'findChapters') { + if (data.chapters && Array.isArray(data.chapters)) { + const list = document.createElement('div'); + list.style.cssText = 'display: flex; flex-direction: column; width: 100%; gap: 5px;'; + + data.chapters.forEach(chap => { + const row = document.createElement('div'); + row.className = 'visual-chapter'; + row.style.cssText = 'padding: 10px; background: var(--bg-surface); border-radius: 4px;'; + row.innerHTML = `Ch. ${chap.chapter} - ${chap.title}
${chap.id}`; + list.appendChild(row); + }); + outputVisual.appendChild(list); + } else { + outputVisual.innerHTML = '
No chapters found. Check JSON.
'; + } + } + else if (type === 'findChapterPages') { + if (Array.isArray(data)) { + data.forEach(page => { + if(page.type === 'text') { + const div = document.createElement('div'); + div.innerHTML = page.content; + div.style.cssText = 'background: #fff; color: #000; padding: 20px; border-radius: 8px; margin-bottom: 20px;'; + outputVisual.appendChild(div); + } else { + const img = document.createElement('img'); + img.src = page.url; + img.style.cssText = 'max-width: 100%; margin-bottom: 10px; border-radius: 4px;'; + outputVisual.appendChild(img); + } + }); + } else { + outputVisual.innerHTML = '
No pages array returned.
'; + } + } + else if (type === 'fetchInfo') { + const container = document.createElement('div'); + container.style.padding = '1rem'; + if (data.fullImage) { + container.innerHTML += ``; + } + container.innerHTML += `
${JSON.stringify(data, null, 2)}
`; + outputVisual.appendChild(container); + } +} \ No newline at end of file diff --git a/src/updateNotification.js b/src/updateNotification.js index 95af66f..a40af0b 100644 --- a/src/updateNotification.js +++ b/src/updateNotification.js @@ -1,6 +1,6 @@ const Gitea_OWNER = 'ItsSkaiya'; const Gitea_REPO = 'WaifuBoard'; -const CURRENT_VERSION = 'v1.6.2'; +const CURRENT_VERSION = 'v1.6.3'; const UPDATE_CHECK_INTERVAL = 5 * 60 * 1000; let currentVersionDisplay; diff --git a/views/emulator.html b/views/emulator.html new file mode 100644 index 0000000..8563ce1 --- /dev/null +++ b/views/emulator.html @@ -0,0 +1,102 @@ + + + + + + + WaifuBoard Extension Emulator + + + + + + + + +
+
+

Extension Emulator

+
+ +
+
+
+

Extension Code

+ Paste your .js file + here +
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+ + + +
+ +
+ +
+
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/views/index.html b/views/index.html index 28583dd..6fac356 100644 --- a/views/index.html +++ b/views/index.html @@ -43,6 +43,10 @@