From 04f37218de431ea916cc734c146d5bc8164d35b6 Mon Sep 17 00:00:00 2001 From: itsskaiya Date: Fri, 21 Nov 2025 11:48:07 -0500 Subject: [PATCH] Updated headless browser to support dynamic sites Removed tabs and moved over to pages Updated the rendering system Fixed multiple pages not loading on scroll and re-rending or not rendering anything or just page 1. Fixed the search bar not taking in spaces for each query Updated how extensions are made Updated how extensions are loaded --- main.js | 1 + src/extensions/load-extensions.js | 35 +++- src/ipc/api-handlers.js | 46 ++++-- src/modules/search-handler.js | 128 ++++++++------- src/preload.js | 2 +- src/renderer.js | 257 +++++++++++++++++++----------- src/utils/headless-browser.js | 75 ++++++--- views/favorites.html | 94 +++++++++++ views/index.html | 130 +++------------ views/settings.html | 91 +++++++++++ views/styles/home.css | 3 + 11 files changed, 567 insertions(+), 295 deletions(-) create mode 100644 views/favorites.html create mode 100644 views/settings.html diff --git a/main.js b/main.js index 1eaade6..c70921f 100644 --- a/main.js +++ b/main.js @@ -62,6 +62,7 @@ function createWindow() { preload: path.join(__dirname, '/src/preload.js'), contextIsolation: true, nodeIntegration: false, + enableRemoteModule: true }, }); diff --git a/src/extensions/load-extensions.js b/src/extensions/load-extensions.js index d7a6272..717c52a 100644 --- a/src/extensions/load-extensions.js +++ b/src/extensions/load-extensions.js @@ -4,23 +4,40 @@ export async function populateSources(sourceList) { sourceList.innerHTML = ''; let initialSource = ''; + console.log("Raw sources received from backend:", sources); + if (sources && sources.length > 0) { sources.forEach((source) => { + if (source.name.toLowerCase().includes('tenor')) { + if (source.type !== 'image-board') { + console.error(`CRITICAL: Tenor extension found, but type is "${source.type}". It will be hidden.`); + } else { + console.log("SUCCESS: Tenor extension passed type check."); + } + } + + if (source.type !== 'image-board') { + return; + } + const button = document.createElement('button'); - button.className = 'source-button hover:bg-gray-700 hover:text-white transition-all duration-200'; - button.dataset.source = source.name; button.title = source.name; 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('.'); + const urlToParse = source.url || source.baseUrl || ""; + if (urlToParse) { + const hostname = new URL(urlToParse).hostname; + const parts = hostname.split('.'); + if (parts.length > 2 && ['api', 'www'].includes(parts[0])) { + mainDomain = parts.slice(1).join('.'); + } else { + mainDomain = hostname; + } } else { - mainDomain = hostname; + mainDomain = source.name; } } catch (e) { mainDomain = source.name; @@ -63,12 +80,14 @@ export async function populateSources(sourceList) { const firstButton = sourceList.children[0]; firstButton.classList.add('active'); initialSource = firstButton.dataset.source; + } else { + console.warn("All sources were filtered out. Check 'type' property in your extensions."); } setupCarousel(sourceList); } else { - console.warn('No sources loaded.'); + console.warn('No sources loaded from API.'); } return initialSource; } diff --git a/src/ipc/api-handlers.js b/src/ipc/api-handlers.js index b5d91af..3937a6d 100644 --- a/src/ipc/api-handlers.js +++ b/src/ipc/api-handlers.js @@ -2,10 +2,11 @@ const fs = require('fs'); const fetchPath = require.resolve('node-fetch'); const cheerioPath = require.resolve('cheerio'); -function peekBaseUrl(filePath) { +function peekProperty(filePath, propertyName) { try { const content = fs.readFileSync(filePath, 'utf-8'); - const match = content.match(/baseUrl\s*=\s*["']([^"']+)["']/); + const regex = new RegExp(`(?:this\\.|^|\\s)${propertyName}\\s*=\\s*["']([^"']+)["']`); + const match = content.match(regex); return match ? match[1] : null; } catch (e) { return null; @@ -16,23 +17,47 @@ module.exports = function (availableScrapers, headlessBrowser) { Object.keys(availableScrapers).forEach(name => { const scraper = availableScrapers[name]; + if (!scraper.url) { - const url = peekBaseUrl(scraper.path); - if (url) { - scraper.url = url; + if (scraper.instance && scraper.instance.baseUrl) { + scraper.url = scraper.instance.baseUrl; + } else { + scraper.url = peekProperty(scraper.path, 'baseUrl'); + } + } + + if (!scraper.type) { + if (scraper.instance && scraper.instance.type) { + scraper.type = scraper.instance.type; + } else { + const typeFromFile = peekProperty(scraper.path, 'type'); + if (typeFromFile) { + console.log(`[API] Recovered type for ${name} via static analysis: ${typeFromFile}`); + scraper.type = typeFromFile; + } } } }); return { getSources: () => { - return Object.keys(availableScrapers).map((name) => { + console.log("[API] Handling getSources request..."); + + const results = Object.keys(availableScrapers).map((name) => { const scraper = availableScrapers[name]; + + const typeToReturn = scraper.type || null; + + console.log(`[API] Processing ${name}: Type found = "${typeToReturn}"`); + return { name: name, - url: scraper.url || name + url: scraper.url || name, + type: typeToReturn }; }); + + return results; }, search: async (event, source, query, page) => { @@ -46,7 +71,6 @@ module.exports = function (availableScrapers, headlessBrowser) { console.log(`[LazyLoad] Initializing scraper: ${source}...`); try { const scraperModule = require(scraperData.path); - const className = Object.keys(scraperModule)[0]; const ScraperClass = scraperModule[className]; @@ -55,12 +79,10 @@ module.exports = function (availableScrapers, headlessBrowser) { } const instance = new ScraperClass(fetchPath, cheerioPath, headlessBrowser); - scraperData.instance = instance; - if (instance.baseUrl) { - scraperData.url = instance.baseUrl; - } + if (instance.type) scraperData.type = instance.type; + if (instance.baseUrl) scraperData.url = instance.baseUrl; } catch (err) { console.error(`Failed to lazy load ${source}:`, err); diff --git a/src/modules/search-handler.js b/src/modules/search-handler.js index 6db3edd..103d327 100644 --- a/src/modules/search-handler.js +++ b/src/modules/search-handler.js @@ -1,4 +1,3 @@ -let currentPage = 1; let hasNextPage = true; let isLoading = false; let currentQuery = ''; @@ -18,10 +17,9 @@ export async function performSearch( return; } - currentPage = 1; hasNextPage = true; isLoading = false; - currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' '); + currentQuery = searchInput.value ? searchInput.value.trim().replace(/[, ]+/g, ' ') : ''; if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden'); @@ -31,11 +29,12 @@ export async function performSearch( searchModal.classList.add('hidden'); - await loadMoreResults(currentSource, currentLayout, domRefs, callbacks); + await loadMoreResults(currentSource, 1, currentLayout, domRefs, callbacks); } export async function loadMoreResults( currentSource, + page, currentLayout, domRefs, callbacks @@ -49,67 +48,76 @@ export async function loadMoreResults( isLoading = true; - if (currentPage === 1) { - loadingSpinner.classList.remove('hidden'); + if (page === 1) { + if(loadingSpinner) loadingSpinner.classList.remove('hidden'); } else { - infiniteLoadingSpinner.classList.remove('hidden'); + if(infiniteLoadingSpinner) infiniteLoadingSpinner.classList.remove('hidden'); } - const result = await window.api.search( - currentSource, - currentQuery, - currentPage - ); + try { + const result = await window.api.search( + currentSource, + currentQuery, + page + ); - loadingSpinner.classList.add('hidden'); - infiniteLoadingSpinner.classList.add('hidden'); + if (loadingSpinner) loadingSpinner.classList.add('hidden'); + if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden'); - if ( - !result.success || - !result.data.results || - result.data.results.length === 0 - ) { - hasNextPage = false; - if (currentPage === 1) { + if ( + !result.success || + !result.data.results || + result.data.results.length === 0 + ) { + hasNextPage = false; + if (page === 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) { + if (page === 1) { + hasNextPage = false; + 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); + applyLayoutToGallery(contentGallery, currentLayout); - contentGallery.innerHTML = - '

No results found. Please try another search term.

'; - } - isLoading = false; - return; + + hasNextPage = result.data.hasNextPage; + + } catch (error) { + console.error("Search/Load Error:", error); + if (loadingSpinner) loadingSpinner.classList.add('hidden'); + if (infiniteLoadingSpinner) infiniteLoadingSpinner.classList.add('hidden'); + } finally { + isLoading = false; } - - 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/preload.js b/src/preload.js index 389eb9b..227646d 100644 --- a/src/preload.js +++ b/src/preload.js @@ -6,7 +6,7 @@ contextBridge.exposeInMainWorld('api', { addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav), removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id), - search: (source, query) => ipcRenderer.invoke('api:search', source, query), + search: (source, query, page) => ipcRenderer.invoke('api:search', source, query, page), toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'), getSources: () => ipcRenderer.invoke('api:getSources'), diff --git a/src/renderer.js b/src/renderer.js index 37a16a9..a1d03c2 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,27 +1,30 @@ import { populateSources } from './extensions/load-extensions.js'; import { setupGlobalKeybinds } from './utils/keybinds.js'; -import { getDomElements } from './utils/dom-loader.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'; +import { applyLayoutToGallery } from './modules/layout-manager.js'; document.addEventListener('DOMContentLoaded', async () => { const domRefs = getDomElements(); + const currentLayout = 'grid'; let currentSource = ''; - let currentLayout = loadSavedLayout(); - - setupGlobalKeybinds(domRefs.searchModal); - + let currentPage = 1; + let isFetching = false; + function showMessage(message, type = 'success') { - uiShowMessage(domRefs.messageBar, message, type); + if (domRefs.messageBar) { + uiShowMessage(domRefs.messageBar, message, type); + } } function showTagModal(tags) { - populateTagModal(domRefs.tagInfoContent, tags); - domRefs.tagInfoModal.classList.remove('hidden'); + if (domRefs.tagInfoContent && domRefs.tagInfoModal) { + populateTagModal(domRefs.tagInfoContent, tags); + domRefs.tagInfoModal.classList.remove('hidden'); + } } function localCreateImageCard(id, tags, imageUrl, thumbnailUrl, type) { @@ -30,16 +33,13 @@ document.addEventListener('DOMContentLoaded', async () => { showMessage, showTagModal, applyLayoutToGallery, - favoritesGallery: domRefs.favoritesGallery + favoritesGallery: document.getElementById('favorites-gallery') }); } function updateHeader() { - if (currentSource) { - domRefs.headerContext.textContent = `Source: ${currentSource}`; - } else { - domRefs.headerContext.textContent = 'No source selected'; - } + if (!domRefs.headerContext) return; + domRefs.headerContext.classList.add('hidden'); } const callbacks = { @@ -49,84 +49,161 @@ document.addEventListener('DOMContentLoaded', async () => { createImageCard: localCreateImageCard }; - function handleNavigation(pageId) { - navShowPage(pageId, domRefs, callbacks, { currentLayout }); + if (domRefs.searchModal) { + setupGlobalKeybinds(domRefs.searchModal); } - 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); + if (domRefs.tagInfoCloseButton && domRefs.tagInfoModal) { + domRefs.tagInfoCloseButton.addEventListener('click', () => { + domRefs.tagInfoModal.classList.add('hidden'); + }); + domRefs.tagInfoModal.addEventListener('click', (e) => { + if (e.target === domRefs.tagInfoModal) { + domRefs.tagInfoModal.classList.add('hidden'); } + }); + } + + if (domRefs.searchIconButton && domRefs.searchModal) { + domRefs.searchIconButton.addEventListener('click', () => { + domRefs.searchModal.classList.remove('hidden'); + if(domRefs.searchInput) { + domRefs.searchInput.focus(); + domRefs.searchInput.select(); + } + }); + + domRefs.searchCloseButton.addEventListener('click', () => { + domRefs.searchModal.classList.add('hidden'); + }); + } + + if (domRefs.sourceList) { + if (domRefs.contentGallery) { + applyLayoutToGallery(domRefs.contentGallery, currentLayout); } - }); - }); - - const initialSource = await populateSources(domRefs.sourceList); - currentSource = initialSource; + let initialSource = ''; + if (window.api && window.api.getSources) { + initialSource = await populateSources(domRefs.sourceList); + } else { + initialSource = await populateSources(domRefs.sourceList); + } + + currentSource = initialSource; + updateHeader(); - updateHeader(); - handleNavigation('browse-page'); + 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; + updateHeader(); + + currentPage = 1; + + if (domRefs.searchInput && domRefs.searchInput.value.trim()) { + performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); + } else if (domRefs.searchInput) { + performSearch(currentSource, { value: "" }, currentLayout, domRefs, callbacks); + } + } + }); + + const scrollContainer = document.querySelector('.content-view'); + if (scrollContainer) { + scrollContainer.addEventListener('scroll', async () => { + if ( + scrollContainer.scrollTop + scrollContainer.clientHeight >= + scrollContainer.scrollHeight - 600 + ) { + if (isFetching) return; + isFetching = true; + + currentPage++; + + if (domRefs.infiniteLoadingSpinner) { + domRefs.infiniteLoadingSpinner.classList.remove('hidden'); + } + + try { + await loadMoreResults(currentSource, currentPage, currentLayout, domRefs, callbacks); + } catch (error) { + console.error("Failed to load more results:", error); + currentPage--; + } finally { + isFetching = false; + if (domRefs.infiniteLoadingSpinner) { + domRefs.infiniteLoadingSpinner.classList.add('hidden'); + } + } + } + }); + } + + if (domRefs.searchButton && domRefs.searchInput) { + domRefs.searchButton.addEventListener('click', () => { + currentPage = 1; + performSearch(currentSource, domRefs.searchInput, currentLayout, domRefs, callbacks); + }); + } + } + + if (document.getElementById('favorites-gallery')) { + const favGallery = document.getElementById('favorites-gallery'); + + const fetchFavorites = async () => { + try { + if (window.api && window.api.getFavorites) { + return await window.api.getFavorites(); + } else { + console.error("window.api.getFavorites is missing."); + return []; + } + } catch (err) { + console.error("Error fetching favorites via IPC:", err); + return []; + } + }; + + const rawFavorites = await fetchFavorites(); + + favGallery.innerHTML = ''; + + if (!rawFavorites || rawFavorites.length === 0) { + const emptyState = document.createElement('div'); + emptyState.className = 'loading-state'; + emptyState.style.gridColumn = '1 / -1'; + emptyState.innerHTML = '

No favorites found.

'; + favGallery.appendChild(emptyState); + } else { + rawFavorites.forEach(row => { + const id = row.id; + const imageUrl = row.image_url; + const thumbnailUrl = row.thumbnail_url; + + let tags = []; + if (typeof row.tags === 'string') { + tags = row.tags.split(',').filter(t => t.trim() !== ''); + } else if (Array.isArray(row.tags)) { + tags = row.tags; + } + + const card = localCreateImageCard( + id, + tags, + imageUrl, + thumbnailUrl, + 'image' + ); + favGallery.appendChild(card); + }); + } + + applyLayoutToGallery(favGallery, currentLayout); + } }); \ No newline at end of file diff --git a/src/utils/headless-browser.js b/src/utils/headless-browser.js index 6b9bc04..e143375 100644 --- a/src/utils/headless-browser.js +++ b/src/utils/headless-browser.js @@ -2,7 +2,14 @@ const { BrowserWindow } = require('electron'); class HeadlessBrowser { async scrape(url, evalFunc, options = {}) { - const { waitSelector = null, timeout = 15000 } = options; + const { + waitSelector = null, + timeout = 15000, + args = [], + scrollToBottom = false, + renderWaitTime = 2000, + loadImages = true + } = options; const win = new BrowserWindow({ show: false, @@ -12,7 +19,7 @@ class HeadlessBrowser { offscreen: true, contextIsolation: false, nodeIntegration: false, - images: false, + images: loadImages, webgl: false, backgroundThrottling: false, }, @@ -23,32 +30,37 @@ class HeadlessBrowser { win.webContents.setUserAgent(userAgent); const session = win.webContents.session; - const filter = { urls: ['*://*/*'] }; - - session.webRequest.onBeforeRequest(filter, (details, callback) => { + session.webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, callback) => { const url = details.url.toLowerCase(); - const blockExtensions = [ - '.css', '.woff', '.woff2', '.ttf', '.svg', '.eot', - 'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem' + '.woff', '.woff2', '.ttf', '.eot', + 'google-analytics', 'doubleclick', 'facebook', 'twitter', 'adsystem' ]; - - const isBlocked = blockExtensions.some(ext => url.includes(ext)); - - if (isBlocked) { - return callback({ cancel: true }); - } - + if (blockExtensions.some(ext => url.includes(ext))) return callback({ cancel: true }); return callback({ cancel: false }); }); await win.loadURL(url, { userAgent }); if (waitSelector) { - await this.waitForSelector(win, waitSelector, timeout); + try { + await this.waitForSelector(win, waitSelector, timeout); + } catch (e) { + console.warn(`[Headless] Timeout waiting for ${waitSelector}, proceeding anyway...`); + } } - const result = await win.webContents.executeJavaScript(`(${evalFunc.toString()})()`); + if (scrollToBottom) { + await this.smoothScrollToBottom(win); + } + + if (renderWaitTime > 0) { + await new Promise(resolve => setTimeout(resolve, renderWaitTime)); + } + + const result = await win.webContents.executeJavaScript( + `(${evalFunc.toString()}).apply(null, ${JSON.stringify(args)})` + ); return result; @@ -70,11 +82,12 @@ class HeadlessBrowser { }, ${timeout}); const check = () => { - if (document.querySelector('${selector}')) { + const el = document.querySelector('${selector}'); + if (el) { clearTimeout(timer); resolve(true); } else { - setTimeout(check, 50); + setTimeout(check, 200); } }; check(); @@ -82,6 +95,30 @@ class HeadlessBrowser { `; await win.webContents.executeJavaScript(script); } + + async smoothScrollToBottom(win) { + const script = ` + new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const maxScrolls = 200; + let currentScrolls = 0; + + const timer = setInterval(() => { + const scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + currentScrolls++; + + if(totalHeight >= scrollHeight - window.innerHeight || currentScrolls >= maxScrolls){ + clearInterval(timer); + resolve(); + } + }, 20); + }); + `; + await win.webContents.executeJavaScript(script); + } } module.exports = new HeadlessBrowser(); \ No newline at end of file diff --git a/views/favorites.html b/views/favorites.html new file mode 100644 index 0000000..8bd0828 --- /dev/null +++ b/views/favorites.html @@ -0,0 +1,94 @@ + + + + + + + Waifu Board - Favorites + + + + + + +
+
+
+ + + + + +
+ + + + +

Favorites

+
+ +
+
+
+

Your Favorites

+

Your personally curated collection.

+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/views/index.html b/views/index.html index 8704039..6bc89f1 100644 --- a/views/index.html +++ b/views/index.html @@ -1,86 +1,68 @@ - - + Waifu Board - + -
-
- + - +
- -

Home

+

Dashboard

-
+

Sources

+
-

- Sources

-
-
- -

- Library

+

Library

- -
- - - - -
@@ -166,31 +95,22 @@ - - - - \ No newline at end of file diff --git a/views/settings.html b/views/settings.html new file mode 100644 index 0000000..af19b76 --- /dev/null +++ b/views/settings.html @@ -0,0 +1,91 @@ + + + + + + + Waifu Board - Settings + + + + + + +
+
+
+ + + + + +
+ + + + +

Settings

+
+ +
+
+
+

Settings

+

App configuration.

+
+ +
+
+

No settings available currently.

+
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/views/styles/home.css b/views/styles/home.css index de45218..f983b96 100644 --- a/views/styles/home.css +++ b/views/styles/home.css @@ -136,6 +136,9 @@ body { margin-right: 1rem; } +a, a:visited, a:hover, a:active { + text-decoration: none; +} .nav-button span { opacity: 0;