Organized all script files
Removed useless things from all files Added an update notification if there is an update Updated the directory for the app icon
This commit is contained in:
77
main.js
77
main.js
@@ -1,37 +1,28 @@
|
||||
/*
|
||||
main.js (Electron Main Process)
|
||||
MODIFIED: Swapped 'better-sqlite3' for 'sqlite3' to remove C++ dependency.
|
||||
*/
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
// --- NEW: Get paths for *both* dependencies ---
|
||||
|
||||
const fetchPath = require.resolve('node-fetch');
|
||||
const cheerioPath = require.resolve('cheerio');
|
||||
// --- END NEW ---
|
||||
|
||||
// --- Core paths ---
|
||||
const waifuBoardsPath = path.join(app.getPath('home'), 'WaifuBoards');
|
||||
const pluginsPath = path.join(waifuBoardsPath, 'extensions');
|
||||
const dbPath = path.join(waifuBoardsPath, 'favorites.db');
|
||||
|
||||
// --- Ensure directories exist ---
|
||||
try {
|
||||
if (!fs.existsSync(waifuBoardsPath)) {
|
||||
fs.mkdirSync(waifuBoardsPath);
|
||||
}
|
||||
if (!fs.existsSync(pluginsPath)) {
|
||||
// Use recursive: true in case WaifuBoards doesn't exist yet
|
||||
|
||||
fs.mkdirSync(pluginsPath, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create directories:', error);
|
||||
// We can probably continue, but loading/saving will fail.
|
||||
|
||||
}
|
||||
|
||||
// --- API Scraper Loader ---
|
||||
// This will hold our instantiated scraper classes, e.g. { 'Gelbooru': new Gelbooru() }
|
||||
const loadedScrapers = {};
|
||||
|
||||
function loadScrapers() {
|
||||
@@ -44,22 +35,19 @@ function loadScrapers() {
|
||||
.forEach((file) => {
|
||||
const filePath = path.join(pluginsPath, file);
|
||||
try {
|
||||
// Dynamically require the scraper file
|
||||
|
||||
const scraperModule = require(filePath);
|
||||
// We assume the export is an object like { Gelbooru: class... }
|
||||
|
||||
const className = Object.keys(scraperModule)[0];
|
||||
const ScraperClass = scraperModule[className];
|
||||
|
||||
// Basic check to see if it's a valid scraper class
|
||||
if (
|
||||
typeof ScraperClass === 'function' &&
|
||||
ScraperClass.prototype.fetchSearchResult
|
||||
) {
|
||||
// --- MODIFIED: Inject *both* paths ---
|
||||
const instance = new ScraperClass(fetchPath, cheerioPath);
|
||||
// --- END MODIFIED ---
|
||||
|
||||
// Store the instance and its baseUrl
|
||||
const instance = new ScraperClass(fetchPath, cheerioPath);
|
||||
|
||||
loadedScrapers[className] = {
|
||||
instance: instance,
|
||||
baseUrl: instance.baseUrl,
|
||||
@@ -75,25 +63,21 @@ function loadScrapers() {
|
||||
}
|
||||
});
|
||||
}
|
||||
// --------------------
|
||||
|
||||
// Load scrapers at startup
|
||||
loadScrapers();
|
||||
|
||||
// --- MODIFIED: Initialize sqlite3 (async) ---
|
||||
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(); // Run migrations after connecting
|
||||
runDatabaseMigrations();
|
||||
}
|
||||
});
|
||||
|
||||
// --- MODIFIED: Database functions are now async ---
|
||||
function runDatabaseMigrations() {
|
||||
db.serialize(() => {
|
||||
// Create the 'favorites' table
|
||||
|
||||
db.run(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
@@ -109,7 +93,6 @@ function runDatabaseMigrations() {
|
||||
}
|
||||
);
|
||||
|
||||
// --- Migration (Add thumbnail_url) ---
|
||||
console.log('Checking database schema for "thumbnail_url"...');
|
||||
db.all('PRAGMA table_info(favorites)', (err, columns) => {
|
||||
if (err) {
|
||||
@@ -137,7 +120,6 @@ function runDatabaseMigrations() {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Migration (Add tags) ---
|
||||
console.log('Checking database schema for "tags" column...');
|
||||
db.all('PRAGMA table_info(favorites)', (err, columns) => {
|
||||
if (err) {
|
||||
@@ -163,42 +145,33 @@ function runDatabaseMigrations() {
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
// Attach the 'preload.js' script to the window
|
||||
// This is the secure way to expose Node.js functions to the renderer (frontend)
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
// contextIsolation is true by default and is a critical security feature
|
||||
|
||||
preload: path.join(__dirname, '/scripts/preload.js'),
|
||||
|
||||
contextIsolation: true,
|
||||
// nodeIntegration should be false
|
||||
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Load the index.html file into the window
|
||||
mainWindow.loadFile('index.html');
|
||||
mainWindow.loadFile('views/index.html');
|
||||
|
||||
// --- Add this line to remove the menu bar ---
|
||||
mainWindow.setMenu(null);
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
app.whenReady().then(() => {
|
||||
// loadScrapers(); // MOVED: This is now called at the top
|
||||
createWindow();
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS.
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') {
|
||||
db.close((err) => {
|
||||
@@ -208,12 +181,7 @@ app.on('window-all-closed', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// --- IPC Handlers (Backend Functions) ---
|
||||
// These functions listen for calls from the 'preload.js' script
|
||||
|
||||
// NEW: Send the list of loaded scrapers to the frontend
|
||||
ipcMain.handle('api:getSources', () => {
|
||||
// Returns an array of objects: [{ name: 'Gelbooru', url: 'https://gelbooru.com' }, ...]
|
||||
return Object.keys(loadedScrapers).map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
@@ -222,12 +190,9 @@ ipcMain.handle('api:getSources', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// MODIFIED: Generic search handler now accepts a page number
|
||||
ipcMain.handle('api:search', async (event, source, query, page) => {
|
||||
try {
|
||||
// Check if the requested source was successfully loaded
|
||||
if (loadedScrapers[source] && loadedScrapers[source].instance) {
|
||||
// Pass the page number to the scraper
|
||||
const results = await loadedScrapers[source].instance.fetchSearchResult(
|
||||
query,
|
||||
page
|
||||
@@ -241,16 +206,12 @@ ipcMain.handle('api:search', async (event, source, query, page) => {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// --- MODIFIED: All db handlers are now async Promises ---
|
||||
|
||||
// Handle request to get all favorites
|
||||
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([]); // Resolve with empty array on error
|
||||
resolve([]);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
@@ -258,7 +219,6 @@ ipcMain.handle('db:getFavorites', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle request to add a favorite
|
||||
ipcMain.handle('db:addFavorite', (event, fav) => {
|
||||
return new Promise((resolve) => {
|
||||
const stmt =
|
||||
@@ -267,7 +227,7 @@ ipcMain.handle('db:addFavorite', (event, fav) => {
|
||||
stmt,
|
||||
[fav.id, fav.title, fav.imageUrl, fav.thumbnailUrl, fav.tags],
|
||||
function (err) {
|
||||
// Must use 'function' to get 'this'
|
||||
|
||||
if (err) {
|
||||
if (err.code.includes('SQLITE_CONSTRAINT')) {
|
||||
resolve({ success: false, error: 'Item is already a favorite.' });
|
||||
@@ -283,12 +243,11 @@ ipcMain.handle('db:addFavorite', (event, fav) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle request to remove a favorite
|
||||
ipcMain.handle('db:removeFavorite', (event, id) => {
|
||||
return new Promise((resolve) => {
|
||||
const stmt = 'DELETE FROM favorites WHERE id = ?';
|
||||
db.run(stmt, id, function (err) {
|
||||
// Must use 'function' to get 'this'
|
||||
|
||||
if (err) {
|
||||
console.error('Error removing favorite:', err.message);
|
||||
resolve({ success: false, error: err.message });
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"productName": "Waifu Board",
|
||||
"win": {
|
||||
"target": "portable",
|
||||
"icon": "build/waifuboards.ico"
|
||||
"icon": "public/waifuboards.ico"
|
||||
},
|
||||
"files": [
|
||||
"**/*",
|
||||
|
||||
21
preload.js
21
preload.js
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
preload.js
|
||||
This script runs in a special, isolated context before the web page (index.html)
|
||||
is loaded. It uses 'contextBridge' to securely expose specific functions
|
||||
from the main process (like database access) to the renderer process (frontend).
|
||||
*/
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose a 'db' object to the global 'window' object in the renderer
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
// --- Database Functions ---
|
||||
getFavorites: () => ipcRenderer.invoke('db:getFavorites'),
|
||||
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
|
||||
removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id),
|
||||
|
||||
// --- API Function ---
|
||||
// This is now a generic search function that takes the source
|
||||
search: (source, query) => ipcRenderer.invoke('api:search', source, query),
|
||||
// NEW: This function gets the list of available sources from main.js
|
||||
getSources: () => ipcRenderer.invoke('api:getSources'),
|
||||
});
|
||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
12
scripts/preload.js
Normal file
12
scripts/preload.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
|
||||
getFavorites: () => ipcRenderer.invoke('db:getFavorites'),
|
||||
addFavorite: (fav) => ipcRenderer.invoke('db:addFavorite', fav),
|
||||
removeFavorite: (id) => ipcRenderer.invoke('db:removeFavorite', id),
|
||||
|
||||
search: (source, query) => ipcRenderer.invoke('api:search', source, query),
|
||||
|
||||
getSources: () => ipcRenderer.invoke('api:getSources'),
|
||||
});
|
||||
@@ -1,65 +1,48 @@
|
||||
/*
|
||||
renderer.js
|
||||
MODIFIED: Now includes infinite scrolling
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// --- Page Elements ---
|
||||
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 favoritesPage = document.getElementById('favorites-page');
|
||||
const settingsPage = document.getElementById('settings-page');
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
const headerContext = document.getElementById('header-context');
|
||||
|
||||
// --- Search Modal Elements ---
|
||||
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');
|
||||
|
||||
// --- Gallery Elements ---
|
||||
const sourceList = document.getElementById('source-list');
|
||||
const contentGallery = document.getElementById('content-gallery');
|
||||
const favoritesGallery = document.getElementById('favorites-gallery');
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
// NEW: Get the infinite loading spinner
|
||||
|
||||
const infiniteLoadingSpinner = document.getElementById(
|
||||
'infinite-loading-spinner'
|
||||
);
|
||||
const messageBar = document.getElementById('message-bar');
|
||||
const galleryPlaceholder = document.getElementById('gallery-placeholder');
|
||||
|
||||
// --- Settings Elements ---
|
||||
const layoutRadios = document.querySelectorAll('input[name="layout"]');
|
||||
const layoutScroll = document.getElementById('layout-scroll');
|
||||
const layoutGrid = document.getElementById('layout-grid');
|
||||
const layoutCompact = document.getElementById('layout-compact');
|
||||
|
||||
// --- Tag Info Modal Elements ---
|
||||
const tagInfoModal = document.getElementById('tag-info-modal');
|
||||
const tagInfoCloseButton = document.getElementById(
|
||||
'tag-info-close-button'
|
||||
);
|
||||
const tagInfoContent = document.getElementById('tag-info-content');
|
||||
|
||||
// --- App State ---
|
||||
let currentFavorites = []; // Cache for favorites
|
||||
let currentFavorites = [];
|
||||
let currentSource = '';
|
||||
let currentQuery = '';
|
||||
let currentLayout = 'scroll'; // Default layout
|
||||
// --- NEW: State for infinite scroll ---
|
||||
let currentLayout = 'scroll';
|
||||
let currentPage = 1;
|
||||
let isLoading = false;
|
||||
let hasNextPage = true;
|
||||
|
||||
// --- Populate Sources Sidebar ---
|
||||
async function populateSources() {
|
||||
console.log('Requesting sources from main process...');
|
||||
const sources = await window.api.getSources(); // e.g., [{ name: 'Gelbooru', url: '...' }]
|
||||
sourceList.innerHTML = ''; // Clear "Loading..."
|
||||
const sources = await window.api.getSources();
|
||||
sourceList.innerHTML = '';
|
||||
|
||||
if (sources && sources.length > 0) {
|
||||
sources.forEach((source) => {
|
||||
@@ -69,32 +52,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
button.dataset.source = source.name;
|
||||
button.title = source.name;
|
||||
|
||||
// Create and add favicon
|
||||
const favicon = document.createElement('img');
|
||||
favicon.className = 'w-8 h-8 rounded';
|
||||
|
||||
// Parse main domain from URL to get correct favicon
|
||||
let mainDomain = source.url; // Default to the full URL
|
||||
let mainDomain = source.url;
|
||||
try {
|
||||
const hostname = new URL(source.url).hostname; // e.g., 'api.waifu.pics'
|
||||
const hostname = new URL(source.url).hostname;
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length > 2 && ['api', 'www'].includes(parts[0])) {
|
||||
// Get the last two parts (e.g., 'waifu.pics' from 'api.waifu.pics')
|
||||
mainDomain = parts.slice(1).join('.');
|
||||
} else {
|
||||
// It's already a main domain (e.g., 'gelbooru.com')
|
||||
mainDomain = hostname;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not parse domain from ${source.url}:`, e);
|
||||
mainDomain = source.name;
|
||||
}
|
||||
// --- END NEW ---
|
||||
|
||||
// Use Google's favicon service. sz=32 requests a 32x32 icon.
|
||||
favicon.src = `https://www.google.com/s2/favicons?domain=${mainDomain}&sz=32`;
|
||||
favicon.alt = source.name;
|
||||
// Fallback in case favicon fails to load
|
||||
favicon.onerror = () => {
|
||||
button.innerHTML = `<span class="font-bold text-sm">${source.name.substring(
|
||||
0,
|
||||
@@ -108,7 +84,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
console.log('Sources populated:', sources);
|
||||
|
||||
// Set first source as active by default
|
||||
if (sourceList.children.length > 0) {
|
||||
const firstButton = sourceList.children[0];
|
||||
firstButton.classList.add('active');
|
||||
@@ -120,11 +95,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Source Selection ---
|
||||
sourceList.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('.source-button');
|
||||
if (button) {
|
||||
// ... (remove/add active class) ...
|
||||
sourceList
|
||||
.querySelectorAll('.source-button')
|
||||
.forEach((btn) => btn.classList.remove('active'));
|
||||
@@ -133,46 +106,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentSource = button.dataset.source;
|
||||
console.log('Source changed to:', currentSource);
|
||||
updateHeader();
|
||||
// Automatically re-search when changing source if a query exists
|
||||
if (currentQuery) {
|
||||
// This will reset the gallery and start a new search
|
||||
performSearch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Tab Switching Logic (Sidebar) ---
|
||||
function showPage(pageId) {
|
||||
// Hide all pages
|
||||
|
||||
document.querySelectorAll('.page').forEach((page) => {
|
||||
page.classList.add('hidden');
|
||||
});
|
||||
// De-activate all icon buttons
|
||||
|
||||
document.querySelectorAll('.nav-button').forEach((tab) => {
|
||||
tab.classList.remove('bg-indigo-600', 'text-white');
|
||||
tab.classList.add('text-gray-400', 'hover:bg-gray-700');
|
||||
});
|
||||
|
||||
// Show the active page
|
||||
const activePage = document.getElementById(pageId);
|
||||
activePage.classList.remove('hidden');
|
||||
|
||||
// Highlight the active icon button
|
||||
let activeTab;
|
||||
if (pageId === 'browse-page') {
|
||||
activeTab = browseButton;
|
||||
pageTitle.textContent = 'Browse';
|
||||
updateHeader(); // Update header context
|
||||
updateHeader();
|
||||
} else if (pageId === 'favorites-page') {
|
||||
activeTab = favoritesButton;
|
||||
pageTitle.textContent = 'Favorites';
|
||||
headerContext.textContent = ''; // Clear context
|
||||
// When switching to favorites, refresh the list
|
||||
headerContext.textContent = '';
|
||||
|
||||
loadFavorites();
|
||||
} else if (pageId === 'settings-page') {
|
||||
activeTab = settingsButton;
|
||||
pageTitle.textContent = 'Settings';
|
||||
headerContext.textContent = ''; // Clear context
|
||||
headerContext.textContent = '';
|
||||
}
|
||||
activeTab.classList.add('bg-indigo-600', 'text-white');
|
||||
activeTab.classList.remove('text-gray-400', 'hover:bg-gray-700');
|
||||
@@ -182,41 +150,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
favoritesButton.addEventListener('click', () => showPage('favorites-page'));
|
||||
settingsButton.addEventListener('click', () => showPage('settings-page'));
|
||||
|
||||
// --- Search Modal Logic ---
|
||||
searchIconButton.addEventListener('click', () => {
|
||||
searchModal.classList.remove('hidden');
|
||||
searchInput.focus(); // Auto-focus the search bar
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
});
|
||||
searchCloseButton.addEventListener('click', () => {
|
||||
searchModal.classList.add('hidden');
|
||||
});
|
||||
searchButton.addEventListener('click', () => {
|
||||
// Sanitize search query to allow multiple tags
|
||||
// MODIFIED: This just calls performSearch, which now handles its own state
|
||||
performSearch();
|
||||
});
|
||||
// Close search modal on Escape key
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
searchModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// --- Tag Info Modal Logic ---
|
||||
tagInfoCloseButton.addEventListener('click', () => {
|
||||
tagInfoModal.classList.add('hidden');
|
||||
});
|
||||
// Close tag modal by clicking the backdrop
|
||||
|
||||
tagInfoModal.addEventListener('click', (e) => {
|
||||
if (e.target === tagInfoModal) {
|
||||
tagInfoModal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Function to show the tag info modal
|
||||
function showTagModal(tags) {
|
||||
tagInfoContent.innerHTML = ''; // Clear old tags
|
||||
tagInfoContent.innerHTML = '';
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
tagInfoContent.innerHTML =
|
||||
@@ -231,7 +194,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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, ' '); // Replace underscores
|
||||
tagPill.textContent = tag.replace(/_/g, ' ');
|
||||
fragment.appendChild(tagPill);
|
||||
}
|
||||
});
|
||||
@@ -239,7 +202,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
tagInfoModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// --- Header Update ---
|
||||
function updateHeader() {
|
||||
if (currentSource) {
|
||||
headerContext.textContent = `Source: ${currentSource}`;
|
||||
@@ -248,56 +210,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Search Function ---
|
||||
async function performSearch() {
|
||||
if (!currentSource) {
|
||||
showMessage('Please select a source from the sidebar.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- NEW: Reset state for a new search ---
|
||||
currentPage = 1;
|
||||
hasNextPage = true;
|
||||
isLoading = false;
|
||||
currentQuery = searchInput.value.trim().replace(/[, ]+/g, ' ');
|
||||
|
||||
if (galleryPlaceholder) galleryPlaceholder.classList.add('hidden');
|
||||
// Clear and apply layout classes
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
contentGallery.innerHTML = ''; // Clear previous results
|
||||
updateHeader(); // Update header to show source
|
||||
|
||||
// Close modal after search
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
contentGallery.innerHTML = '';
|
||||
updateHeader();
|
||||
|
||||
searchModal.classList.add('hidden');
|
||||
|
||||
// Load the first page of results
|
||||
loadMoreResults();
|
||||
}
|
||||
|
||||
// --- NEW: Infinite Scroll Loader ---
|
||||
async function loadMoreResults() {
|
||||
// Don't load if we're already loading or if there are no more pages
|
||||
|
||||
if (isLoading || !hasNextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
// Show the correct spinner
|
||||
if (currentPage === 1) {
|
||||
loadingSpinner.classList.remove('hidden'); // Show main spinner
|
||||
loadingSpinner.classList.remove('hidden');
|
||||
} else {
|
||||
infiniteLoadingSpinner.classList.remove('hidden'); // Show bottom spinner
|
||||
infiniteLoadingSpinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Use the new API function with the current page
|
||||
const result = await window.api.search(
|
||||
currentSource,
|
||||
currentQuery,
|
||||
currentPage
|
||||
);
|
||||
|
||||
// Hide all spinners
|
||||
loadingSpinner.classList.add('hidden');
|
||||
infiniteLoadingSpinner.classList.add('hidden');
|
||||
|
||||
@@ -306,14 +260,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
!result.data.results ||
|
||||
result.data.results.length === 0
|
||||
) {
|
||||
hasNextPage = false; // Stop trying to load more
|
||||
hasNextPage = false;
|
||||
if (currentPage === 1) {
|
||||
// If it's the first page and no results, show "No results"
|
||||
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
contentGallery.innerHTML =
|
||||
'<p class="text-gray-400 text-center text-lg">No results found. Please try another search term.</p>';
|
||||
}
|
||||
// If it's not the first page, we just stop loading (no message needed)
|
||||
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
@@ -321,7 +275,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const validResults = result.data.results.filter((item) => item.image);
|
||||
|
||||
if (validResults.length === 0) {
|
||||
hasNextPage = false; // Stop trying to load more
|
||||
hasNextPage = false;
|
||||
if (currentPage === 1) {
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
contentGallery.innerHTML =
|
||||
@@ -331,66 +285,57 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a DocumentFragment for performance
|
||||
const fragment = document.createDocumentFragment();
|
||||
validResults.forEach((item) => {
|
||||
const thumbnailUrl = item.image;
|
||||
// const fullImageUrl = getFullImageUrl(thumbnailUrl, currentSource);
|
||||
|
||||
const displayUrl = item.sampleImageUrl || item.fullImageUrl || thumbnailUrl;
|
||||
|
||||
const card = createImageCard(
|
||||
item.id.toString(),
|
||||
item.tags, // Pass the whole tags array
|
||||
displayUrl, // Pass the new *real* URL
|
||||
thumbnailUrl, // Pass the *real* thumbnail as a fallback
|
||||
item.tags,
|
||||
displayUrl,
|
||||
thumbnailUrl,
|
||||
'browse'
|
||||
);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
// Append the new results instead of overwriting
|
||||
|
||||
contentGallery.appendChild(fragment);
|
||||
|
||||
// Update state for the next scroll
|
||||
hasNextPage = result.data.hasNextPage;
|
||||
currentPage++;
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
// --- NEW: Scroll Event Listener for Browse Page ---
|
||||
browsePage.addEventListener('scroll', () => {
|
||||
// Check if user is near the bottom of the scrollable area
|
||||
if (
|
||||
browsePage.scrollTop + browsePage.clientHeight >=
|
||||
browsePage.scrollHeight - 600 // Load 600px before the end
|
||||
browsePage.scrollHeight - 600
|
||||
) {
|
||||
loadMoreResults();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Favorites Logic ---
|
||||
async function loadFavorites() {
|
||||
// Apply layout classes
|
||||
applyLayoutToGallery(favoritesGallery, currentLayout);
|
||||
favoritesGallery.innerHTML =
|
||||
'<div class="text-center p-10"><p class="text-gray-400">Loading favorites...</p></div>';
|
||||
currentFavorites = await window.api.getFavorites();
|
||||
|
||||
if (currentFavorites.length === 0) {
|
||||
// Apply layout classes
|
||||
applyLayoutToGallery(favoritesGallery, currentLayout);
|
||||
favoritesGallery.innerHTML =
|
||||
'<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply layout classes
|
||||
applyLayoutToGallery(favoritesGallery, currentLayout);
|
||||
favoritesGallery.innerHTML = ''; // Clear loading message
|
||||
favoritesGallery.innerHTML = '';
|
||||
const fragment = document.createDocumentFragment();
|
||||
currentFavorites.forEach((fav) => {
|
||||
const card = createImageCard(
|
||||
fav.id,
|
||||
// Read from the new 'tags' column instead of 'title'
|
||||
fav.tags ? fav.tags.split(',') : [],
|
||||
fav.image_url,
|
||||
fav.thumbnail_url,
|
||||
@@ -402,11 +347,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
async function handleAddFavorite(id, tags, imageUrl, thumbnailUrl) {
|
||||
// Ensure 'tags' is an array before using array methods
|
||||
const safeTags = Array.isArray(tags) ? tags : [];
|
||||
// Title is just the first tag (or a default), for simplicity
|
||||
const title = safeTags.length > 0 ? safeTags[0] : 'Favorite';
|
||||
// Create a string of all tags to store
|
||||
const allTags = safeTags.join(',');
|
||||
|
||||
const result = await window.api.addFavorite({
|
||||
@@ -414,7 +356,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
title,
|
||||
imageUrl,
|
||||
thumbnailUrl,
|
||||
tags: allTags, // Pass all tags to the backend
|
||||
tags: allTags,
|
||||
});
|
||||
if (result.success) {
|
||||
showMessage('Added to favorites!', 'success');
|
||||
@@ -427,7 +369,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const result = await window.api.removeFavorite(id);
|
||||
if (result.success) {
|
||||
showMessage('Removed from favorites.', 'success');
|
||||
// Find the card to remove, regardless of layout
|
||||
const cardToRemove = document.querySelector(
|
||||
`#favorites-gallery [data-id='${id}']`
|
||||
);
|
||||
@@ -436,50 +377,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => {
|
||||
cardToRemove.remove();
|
||||
if (favoritesGallery.children.length === 0) {
|
||||
// Apply layout classes
|
||||
applyLayoutToGallery(favoritesGallery, currentLayout);
|
||||
favoritesGallery.innerHTML =
|
||||
'<p class="text-gray-400 text-center text-lg">You haven\'t saved any favorites yet.</p>';
|
||||
}
|
||||
}, 300); // Wait for animation
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
showMessage(result.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI Helpers ---
|
||||
|
||||
/**
|
||||
* REWRITTEN: Creates a professional image card based on current layout.
|
||||
* @param {string} id - The unique ID of the artwork.
|
||||
* @param {string[]} tags - An array of tags.
|
||||
* @param {string} imageUrl - The full URL of the image to display.
|
||||
* @param {string} thumbnailUrl - The fallback thumbnail URL.
|
||||
* @param {'browse' | 'fav'} type - The type of card to create.
|
||||
* @returns {HTMLElement} The card element.
|
||||
*/
|
||||
function createImageCard(id, tags, imageUrl, thumbnailUrl, type) {
|
||||
// Ensure 'tags' is an array before using array methods
|
||||
|
||||
const safeTags = Array.isArray(tags) ? tags : [];
|
||||
|
||||
// --- All layouts use this as the base card ---
|
||||
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`;
|
||||
|
||||
// --- "Compact" layout gets a special style ---
|
||||
if (currentLayout === 'compact') {
|
||||
// Use aspect-ratio to keep cards square and uniform
|
||||
|
||||
entry.classList.add('aspect-square');
|
||||
}
|
||||
|
||||
// Image container with pulse animation for loading
|
||||
const imageContainer = document.createElement('div');
|
||||
imageContainer.className =
|
||||
'w-full bg-gray-700 animate-pulse relative';
|
||||
|
||||
// For "Compact" layout, image container is also square
|
||||
if (currentLayout === 'compact') {
|
||||
imageContainer.classList.add('h-full');
|
||||
} else {
|
||||
@@ -489,13 +414,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
entry.appendChild(imageContainer);
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = imageUrl; // Try to load the full-res image first
|
||||
img.alt = safeTags.join(', '); // Use safeTags
|
||||
img.className = 'w-full h-auto object-contain bg-gray-900 opacity-0'; // Start hidden
|
||||
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';
|
||||
|
||||
// "Compact" layout uses "object-cover" to fill the square
|
||||
if (currentLayout === 'compact') {
|
||||
img.className = 'w-full h-full object-cover bg-gray-900 opacity-0';
|
||||
}
|
||||
@@ -508,20 +432,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
img.onerror = () => {
|
||||
console.warn(`Failed to load full image: ${imageUrl}. Falling back to thumbnail.`);
|
||||
img.src = thumbnailUrl; // Fallback
|
||||
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; // Prevent infinite loop
|
||||
img.onerror = null;
|
||||
};
|
||||
imageContainer.appendChild(img);
|
||||
|
||||
// --- Add buttons (overlay on hover) ---
|
||||
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';
|
||||
|
||||
// Add Info Button
|
||||
buttonContainer.appendChild(createInfoButton(safeTags));
|
||||
|
||||
if (type === 'browse') {
|
||||
@@ -531,49 +453,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
buttonContainer.appendChild(createRemoveFavoriteButton(id));
|
||||
}
|
||||
imageContainer.appendChild(buttonContainer); // Add buttons to image container
|
||||
imageContainer.appendChild(buttonContainer);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to guess the full-resolution image URL from a thumbnail URL.
|
||||
* This is a "best guess" based on common patterns.
|
||||
* @param {string} thumbnailUrl - The URL of the thumbnail.
|
||||
* @param {string} source - The name of the source (e.g., 'Gelbooru', 'Rule34').
|
||||
* @returns {string} The guessed full-resolution URL.
|
||||
*/
|
||||
function getFullImageUrl(thumbnailUrl, source) {
|
||||
if (!thumbnailUrl) return '';
|
||||
|
||||
try {
|
||||
// Waifu.pics API already provides the full URL
|
||||
|
||||
if (source === 'WaifuPics') {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
// Rule34 (API): preview_url -> file_url
|
||||
if (source === 'Rule34' && thumbnailUrl.includes('thumbnail_')) {
|
||||
return thumbnailUrl
|
||||
.replace('/thumbnails/', '/images/')
|
||||
.replace('thumbnail_', '');
|
||||
}
|
||||
|
||||
// Gelbooru (Scraper): /thumbnails/ -> /images/
|
||||
if (source === 'Gelbooru' && thumbnailUrl.includes('/thumbnails/')) {
|
||||
return thumbnailUrl
|
||||
.replace('/thumbnails/', '/images/')
|
||||
.replace('thumbnail_', '');
|
||||
}
|
||||
|
||||
// Safebooru (Scraper): /thumbnails/ -> /images/
|
||||
if (source === 'Safebooru' && thumbnailUrl.includes('/thumbnails/')) {
|
||||
return thumbnailUrl
|
||||
.replace('/thumbnails/', '/images/')
|
||||
.replace('thumbnail_', '');
|
||||
}
|
||||
|
||||
// Fallback for unknown scrapers
|
||||
if (thumbnailUrl.includes('/thumbnails/')) {
|
||||
return thumbnailUrl
|
||||
.replace('/thumbnails/', '/images/')
|
||||
@@ -583,11 +494,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} catch (e) {
|
||||
console.error('Error parsing full image URL:', e);
|
||||
}
|
||||
// If no rules match, just return the thumbnail URL
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
// --- Button Creation Helpers ---
|
||||
function createInfoButton(safeTags) {
|
||||
const button = document.createElement('button');
|
||||
button.title = 'Show Info';
|
||||
@@ -597,7 +507,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>`;
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
e.stopPropagation();
|
||||
showTagModal(safeTags);
|
||||
};
|
||||
return button;
|
||||
@@ -612,7 +522,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.31h5.518a.562.562 0 01.31.95l-4.203 3.03a.563.563 0 00-.182.53l1.501 4.87a.562.562 0 01-.82.624l-4.204-3.03a.563.563 0 00-.576 0l-4.204 3.03a.562.562 0 01-.82-.624l1.501-4.87a.563.563 0 00-.182-.53L2.498 9.87a.562.562 0 01.31-.95h5.518a.563.563 0 00.475-.31L11.48 3.5z" />
|
||||
</svg>`;
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
e.stopPropagation();
|
||||
handleAddFavorite(id, safeTags, imageUrl, thumbnailUrl);
|
||||
};
|
||||
return button;
|
||||
@@ -627,23 +537,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.578 0a48.108 48.108 0 01-3.478-.397m15.408 0l-2.147-2.147A1.125 1.125 0 0016.34 3H7.66a1.125 1.125 0 00-.795.325L4.772 5.79m14.456 0l-2.29-2.29a1.125 1.125 0 00-.795-.324H8.455a1.125 1.125 0 00-.795.324L5.37 5.79m13.84 0L20.25 7.5" />
|
||||
</svg>`;
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
e.stopPropagation();
|
||||
handleRemoveFavorite(id);
|
||||
};
|
||||
return button;
|
||||
}
|
||||
// --- END NEW: Button Creation Helpers ---
|
||||
|
||||
/**
|
||||
* Shows a green/red message bar at the bottom of the screen.
|
||||
* @param {string} message - The text to display.
|
||||
* @param {'success' | 'error'} type - The type of message.
|
||||
*/
|
||||
function showMessage(message, type = 'success') {
|
||||
if (!messageBar) return;
|
||||
messageBar.textContent = message;
|
||||
|
||||
// Set color
|
||||
if (type === 'error') {
|
||||
messageBar.classList.remove('bg-green-600');
|
||||
messageBar.classList.add('bg-red-600');
|
||||
@@ -652,28 +555,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
messageBar.classList.add('bg-green-600');
|
||||
}
|
||||
|
||||
// Show
|
||||
messageBar.classList.remove('hidden', 'translate-y-16');
|
||||
|
||||
// Hide after 3 seconds
|
||||
setTimeout(() => {
|
||||
messageBar.classList.add('hidden', 'translate-y-16');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// --- NEW: Settings Logic ---
|
||||
function loadSettings() {
|
||||
const savedLayout = localStorage.getItem('waifuBoardLayout') || 'scroll';
|
||||
currentLayout = savedLayout;
|
||||
|
||||
// Check if the saved layout element exists before trying to check it
|
||||
const savedRadio = document.querySelector(
|
||||
`input[name="layout"][value="${savedLayout}"]`
|
||||
);
|
||||
if (savedRadio) {
|
||||
savedRadio.checked = true;
|
||||
} else {
|
||||
// Fallback if saved layout is invalid
|
||||
|
||||
document.getElementById('layout-scroll').checked = true;
|
||||
currentLayout = 'scroll';
|
||||
localStorage.setItem('waifuBoardLayout', 'scroll');
|
||||
@@ -686,14 +585,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentLayout = newLayout;
|
||||
console.log('Layout changed to:', newLayout);
|
||||
|
||||
// Re-render the current view
|
||||
if (browsePage.classList.contains('hidden')) {
|
||||
loadFavorites(); // Re-render favorites
|
||||
loadFavorites();
|
||||
} else {
|
||||
// --- FIX ---
|
||||
// Only re-run the search if there was a query.
|
||||
|
||||
if (currentQuery) {
|
||||
performSearch(); // Re-render browse (will reset to page 1)
|
||||
performSearch();
|
||||
} else {
|
||||
applyLayoutToGallery(contentGallery, currentLayout);
|
||||
}
|
||||
@@ -701,16 +598,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function applyLayoutToGallery(galleryElement, layout) {
|
||||
// Reset all layout classes
|
||||
galleryElement.className = 'p-4 w-full'; // Base classes
|
||||
galleryElement.className = 'p-4 w-full';
|
||||
|
||||
if (layout === 'scroll') {
|
||||
galleryElement.classList.add('max-w-3xl', 'mx-auto', 'space-y-8');
|
||||
} else if (layout === 'grid') {
|
||||
// Use the Masonry layout class
|
||||
galleryElement.classList.add('gallery-masonry');
|
||||
} else if (layout === 'compact') {
|
||||
// Use the standard grid layout (formerly 'gallery-grid')
|
||||
galleryElement.classList.add('gallery-grid');
|
||||
}
|
||||
}
|
||||
@@ -718,10 +612,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
layoutRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', handleLayoutChange);
|
||||
});
|
||||
// --- END NEW: Settings Logic ---
|
||||
|
||||
// --- Initial Load ---
|
||||
loadSettings(); // NEW: Load settings on startup
|
||||
populateSources(); // Load the sources into the dropdown on startup
|
||||
showPage('browse-page'); // Show the browse page on startup
|
||||
loadSettings();
|
||||
populateSources();
|
||||
showPage('browse-page');
|
||||
});
|
||||
77
scripts/updateNotification.js
Normal file
77
scripts/updateNotification.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const GITHUB_OWNER = 'ItsSkaiya';
|
||||
const GITHUB_REPO = 'WaifuBoard';
|
||||
const CURRENT_VERSION = '1.0.0';
|
||||
|
||||
let currentVersionDisplay;
|
||||
let latestVersionDisplay;
|
||||
let updateToast;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
currentVersionDisplay = document.getElementById('currentVersionDisplay');
|
||||
latestVersionDisplay = document.getElementById('latestVersionDisplay');
|
||||
updateToast = document.getElementById('updateToast');
|
||||
|
||||
if (currentVersionDisplay) {
|
||||
currentVersionDisplay.textContent = CURRENT_VERSION;
|
||||
}
|
||||
|
||||
checkForUpdates();
|
||||
});
|
||||
|
||||
function showToast(latestVersion) {
|
||||
|
||||
if (latestVersionDisplay && updateToast) {
|
||||
latestVersionDisplay.textContent = latestVersion;
|
||||
updateToast.classList.add('update-available');
|
||||
updateToast.classList.remove('hidden');
|
||||
|
||||
} else {
|
||||
console.error("Error: Cannot display toast because one or more DOM elements were not found.");
|
||||
}
|
||||
}
|
||||
|
||||
function isVersionOutdated(versionA, versionB) {
|
||||
|
||||
const vA = versionA.replace(/^v/, '').split('.').map(Number);
|
||||
const vB = versionB.replace(/^v/, '').split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
const numA = vA[i] || 0;
|
||||
const numB = vB[i] || 0;
|
||||
|
||||
if (numA < numB) return true;
|
||||
if (numA > numB) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
console.log(`Checking for updates for ${GITHUB_OWNER}/${GITHUB_REPO}...`);
|
||||
const apiUrl = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`;
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const latestVersion = data.tag_name;
|
||||
console.log(`Latest GitHub Release: ${latestVersion}`);
|
||||
|
||||
if (isVersionOutdated(CURRENT_VERSION, latestVersion)) {
|
||||
console.warn('Update available!');
|
||||
showToast(latestVersion);
|
||||
} else {
|
||||
console.info('Package is up to date.');
|
||||
hideToast();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub release:', error);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// --- Configuration ---
|
||||
const GITHUB_OWNER = 'ItsSkaiya'; // e.g., 'google'
|
||||
const GITHUB_REPO = 'WaifuBoard'; // e.g., 'gemini-api-cookbook'
|
||||
const CURRENT_VERSION = '1.0.0'; // Manually set this, or pull from a package.json/config file
|
||||
|
||||
// --- DOM Elements ---
|
||||
const currentVersionDisplay = document.getElementById('currentVersionDisplay');
|
||||
const latestVersionDisplay = document.getElementById('latestVersionDisplay');
|
||||
const updateToast = document.getElementById('updateToast');
|
||||
|
||||
// Display the current version on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
currentVersionDisplay.textContent = CURRENT_VERSION;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Shows the update notification toast.
|
||||
* @param {string} latestVersion - The latest version string from GitHub.
|
||||
*/
|
||||
function showToast(latestVersion) {
|
||||
latestVersionDisplay.textContent = latestVersion;
|
||||
updateToast.classList.add('update-available');
|
||||
updateToast.classList.remove('hidden');
|
||||
// NOTE: The toast will NOT close until the user clicks 'X'
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the update notification toast.
|
||||
*/
|
||||
function hideToast() {
|
||||
updateToast.classList.add('hidden');
|
||||
updateToast.classList.remove('update-available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings (e.g., "1.2.3" vs "1.2.4").
|
||||
* Returns true if version A is older than version B.
|
||||
* @param {string} versionA - The current version.
|
||||
* @param {string} versionB - The latest version.
|
||||
* @returns {boolean} True if A is older than B.
|
||||
*/
|
||||
function isVersionOutdated(versionA, versionB) {
|
||||
// Clean up version strings (e.g., remove 'v' prefix) and split by '.'
|
||||
const vA = versionA.replace(/^v/, '').split('.').map(Number);
|
||||
const vB = versionB.replace(/^v/, '').split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
const numA = vA[i] || 0;
|
||||
const numB = vB[i] || 0;
|
||||
|
||||
if (numA < numB) return true; // A is older
|
||||
if (numA > numB) return false; // A is newer
|
||||
}
|
||||
|
||||
return false; // Versions are the same or incomparable
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to fetch the latest GitHub release and check for updates.
|
||||
*/
|
||||
async function checkForUpdates() {
|
||||
console.log(`Checking for updates for ${GITHUB_OWNER}/${GITHUB_REPO}...`);
|
||||
const apiUrl = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`;
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// The tag_name often contains the version (e.g., "v1.0.1")
|
||||
const latestVersion = data.tag_name;
|
||||
console.log(`Latest GitHub Release: ${latestVersion}`);
|
||||
|
||||
if (isVersionOutdated(CURRENT_VERSION, latestVersion)) {
|
||||
// Package is out of date! Issue the red toast notification.
|
||||
console.warn('Update available!');
|
||||
showToast(latestVersion);
|
||||
} else {
|
||||
// Package is up to date or newer. Do not show the toast.
|
||||
console.info('Package is up to date.');
|
||||
hideToast(); // Ensure it's hidden in case a previous check showed it
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub release:', error);
|
||||
// You might want a different toast here for a failure notification
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,12 @@
|
||||
" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Waifu Board</title>
|
||||
<!-- We'll use Tailwind CSS for a modern, professional look -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
/* Simple scrollbar styling for the dark theme */
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #111827;
|
||||
/* Tailwind gray-900 */
|
||||
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -28,172 +26,149 @@
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
/* gray-800 */
|
||||
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
/* gray-600 */
|
||||
|
||||
border-radius: 5px;
|
||||
border: 2px solid #1f2937;
|
||||
/* gray-800 */
|
||||
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
/* gray-500 */
|
||||
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Simple focus ring for dark mode */
|
||||
|
||||
.dark-focus-ring:focus {
|
||||
outline: 2px solid #4f46e5;
|
||||
/* indigo-600 */
|
||||
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Style for the active source button in the sidebar */
|
||||
|
||||
.source-button.active {
|
||||
background-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
|
||||
color: white;
|
||||
/* Add a ring to the active favicon */
|
||||
|
||||
outline: 2px solid #4f46e5;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Group hover for showing buttons on image cards */
|
||||
|
||||
.image-entry:hover .image-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- MODIFIED --- */
|
||||
/* This is the new "Compact" layout (formerly "Grid") */
|
||||
/* It uses a standard, uniform grid */
|
||||
|
||||
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* --- NEW --- */
|
||||
/* This is the new "Grid" layout (Masonry/Waterfall) */
|
||||
/* It uses CSS columns to create a height-responsive layout */
|
||||
|
||||
|
||||
|
||||
.gallery-masonry {
|
||||
/* Set column count based on screen size */
|
||||
|
||||
column-count: 2;
|
||||
/* 2 columns on small screens */
|
||||
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.gallery-masonry {
|
||||
column-count: 3;
|
||||
/* 3 columns on medium screens */
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.gallery-masonry {
|
||||
column-count: 4;
|
||||
/* 4 columns on large screens */
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* This tells each card in Masonry how to behave */
|
||||
|
||||
.gallery-masonry .image-entry {
|
||||
display: inline-block;
|
||||
/* Respect the column flow */
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
/* Vertical gap */
|
||||
|
||||
break-inside: avoid;
|
||||
/* Prevent cards from breaking across columns */
|
||||
|
||||
}
|
||||
|
||||
/* --- Toast Notification Styling --- */
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px 15px 15px;
|
||||
/* Extra padding on the right for the close button */
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
|
||||
display: flex;
|
||||
/* Allows 'X' button to align to the right */
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 300px;
|
||||
align-items: flex-start;
|
||||
|
||||
min-width: 350px;
|
||||
z-index: 1000;
|
||||
/* Ensure it's above other elements */
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Red style for the 'out of date' notification */
|
||||
.toast.update-available {
|
||||
background-color: #e53935;
|
||||
/* A strong red */
|
||||
}
|
||||
|
||||
/* State for hiding the toast */
|
||||
.toast.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
/* Prevents interaction when hidden */
|
||||
.toast-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.toast p {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
/* Allows the text to take up most of the space */
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
/* Styling for the close button */
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
padding: 5px;
|
||||
line-height: 1;
|
||||
.toast.update-available {
|
||||
background-color: #e53935;
|
||||
}
|
||||
|
||||
.toast.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-gray-200 flex h-screen overflow-hidden">
|
||||
<!-- Sidebar: Main Navigation -->
|
||||
<nav class="w-20 bg-gray-900 flex flex-col items-center flex-shrink-0 p-4 space-y-6">
|
||||
<!-- Home / Browse -->
|
||||
<button id="browse-button" class="nav-button p-3 rounded-xl bg-indigo-600 text-white" title="Browse">
|
||||
<!-- Heroicon: Home -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Favorites -->
|
||||
<button id="favorites-button" class="nav-button p-3 rounded-xl text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||
title="Favorites">
|
||||
<!-- Heroicon: Star -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.31h5.518a.562.562 0 01.31.95l-4.203 3.03a.563.563 0 00-.182.53l1.501 4.87a.562.562 0 01-.82.624l-4.204-3.03a.563.563 0 00-.576 0l-4.204 3.03a.562.562 0 01-.82-.624l1.501-4.87a.563.563 0 00-.182-.53L2.498 9.87a.562.562 0 01.31-.95h5.518a.563.563 0 00.475-.31L11.48 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Search -->
|
||||
<button id="search-icon-button" class="nav-button p-3 rounded-xl text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||
title="Search">
|
||||
<!-- Heroicon: Magnifying Glass -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -201,10 +176,8 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- NEW: Settings Button -->
|
||||
<button id="settings-button" class="nav-button p-3 rounded-xl text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||
title="Settings">
|
||||
<!-- Heroicon: Cog 6 Tooth -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -212,31 +185,21 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="h-px w-10 bg-gray-700"></div>
|
||||
|
||||
<!-- Source List (Dynamically Populated) -->
|
||||
<div id="source-list" class="flex flex-col items-center space-y-4" aria-label="Sources">
|
||||
<!-- Source buttons will be injected here by renderer.js -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Contextual Header -->
|
||||
<header class="bg-gray-800 flex-shrink-0 flex items-center justify-between p-4 border-b border-gray-700 h-[69px]">
|
||||
<h1 id="page-title" class="text-xl font-bold text-gray-100">Browse</h1>
|
||||
<div id="header-context" class="text-sm text-gray-400"></div>
|
||||
</header>
|
||||
|
||||
<!-- Page: Browse -->
|
||||
<!-- MODIFIED: Changed overflow-hidden to overflow-y-auto -->
|
||||
<div id="browse-page" class="page flex-1 overflow-y-auto">
|
||||
<!-- Content Gallery: Layout classes will be added by JS -->
|
||||
<main id="content-gallery" class="p-4 w-full" aria-live="polite">
|
||||
<!-- Loading spinner -->
|
||||
<div id="loading-spinner" class="hidden text-center p-10 text-gray-400">
|
||||
<!-- Tailwind Spinner -->
|
||||
<svg class="animate-spin h-8 w-8 text-indigo-400 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
@@ -246,14 +209,11 @@
|
||||
</svg>
|
||||
<p class="mt-2">Loading...</p>
|
||||
</div>
|
||||
<!-- Search results will be injected here -->
|
||||
<p id="gallery-placeholder" class="text-gray-400 text-center text-lg">
|
||||
Select a source and click the search icon to browse.
|
||||
</p>
|
||||
|
||||
<!-- NEW: Infinite Scroll Loading Spinner -->
|
||||
<div id="infinite-loading-spinner" class="hidden text-center p-10 text-gray-400">
|
||||
<!-- Tailwind Spinner -->
|
||||
<svg class="animate-spin h-8 w-8 text-indigo-400 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
@@ -266,27 +226,20 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Page: Favorites -->
|
||||
<!-- MODIFIED: Changed overflow-hidden to overflow-y-auto -->
|
||||
<div id="favorites-page" class="page hidden flex-1 overflow-y-auto">
|
||||
<!-- Favorites Gallery: Layout classes will be added by JS -->
|
||||
<main id="favorites-gallery" class="p-4 w-full">
|
||||
<!-- Favorite items will be injected here -->
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- NEW: Settings Page -->
|
||||
<div id="settings-page" class="page hidden flex-1 overflow-y-auto p-8">
|
||||
<div class="max-w-2xl mx-auto space-y-8">
|
||||
<h2 class="text-2xl font-bold text-white">Settings</h2>
|
||||
|
||||
<!-- Layout Options -->
|
||||
<div class="bg-gray-800 rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-100 mb-4">
|
||||
Gallery Layout
|
||||
</h3>
|
||||
<fieldset class="space-y-4">
|
||||
<!-- Scroll -->
|
||||
<label for="layout-scroll" class="flex items-center p-4 bg-gray-700 rounded-lg cursor-pointer">
|
||||
<input type="radio" id="layout-scroll" name="layout" value="scroll"
|
||||
class="h-5 w-5 text-indigo-600 border-gray-500 focus:ring-indigo-500" />
|
||||
@@ -295,7 +248,6 @@
|
||||
<span class="text-sm text-gray-400">A single, vertical column of large images.</span>
|
||||
</span>
|
||||
</label>
|
||||
<!-- Grid -->
|
||||
<label for="layout-grid" class="flex items-center p-4 bg-gray-700 rounded-lg cursor-pointer">
|
||||
<input type="radio" id="layout-grid" name="layout" value="grid"
|
||||
class="h-5 w-5 text-indigo-600 border-gray-500 focus:ring-indigo-500" />
|
||||
@@ -305,7 +257,6 @@
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- MODIFIED: "Table" is now "Compact" -->
|
||||
<label for="layout-compact" class="flex items-center p-4 bg-gray-700 rounded-lg cursor-pointer">
|
||||
<input type="radio" id="layout-compact" name="layout" value="compact"
|
||||
class="h-5 w-5 text-indigo-600 border-gray-500 focus:ring-indigo-500" />
|
||||
@@ -320,12 +271,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div id="search-modal"
|
||||
class="hidden fixed inset-0 z-30 bg-black/70 backdrop-blur-md flex items-start justify-center p-8">
|
||||
<div class="bg-gray-800 p-4 rounded-lg shadow-xl w-full max-w-lg relative">
|
||||
<button id="search-close-button" class="absolute top-3 right-3 p-2 text-gray-400 hover:text-white">
|
||||
<!-- Heroicon: X Mark -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -343,13 +292,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEW: Tag Info Modal -->
|
||||
<div id="tag-info-modal"
|
||||
class="hidden fixed inset-0 z-30 bg-black/70 backdrop-blur-md flex items-start justify-center p-8"
|
||||
aria-modal="true">
|
||||
<div class="bg-gray-800 p-6 rounded-lg shadow-xl w-full max-w-lg relative">
|
||||
<button id="tag-info-close-button" class="absolute top-3 right-3 p-2 text-gray-400 hover:text-white">
|
||||
<!-- Heroicon: X Mark -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -357,25 +304,21 @@
|
||||
</button>
|
||||
<h2 class="text-xl font-semibold mb-4">Image Tags</h2>
|
||||
<div id="tag-info-content" class="flex flex-wrap gap-2 max-h-[60vh] overflow-y-auto">
|
||||
<!-- Tags will be injected here by renderer.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Bar (for confirmations) -->
|
||||
<div id="message-bar"
|
||||
class="hidden fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-xl transition-all duration-300 transform translate-y-16">
|
||||
Message
|
||||
</div>
|
||||
|
||||
<div id="updateToast" class="toast hidden">
|
||||
<p>🚨 A new update is available! Version <span id="latestVersionDisplay"></span></p>
|
||||
<button class="close-btn" onclick="hideToast()">X</button>
|
||||
<p>An update is required for Waifu Board! newest version - <span id="latestVersionDisplay"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- Renderer Script -->
|
||||
<script src="./renderer.js"></script>
|
||||
<script src="./updateNotification.js"></script>
|
||||
<script src="../scripts/renderer.js"></script>
|
||||
<script src="../scripts/updateNotification.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user