diff options
-rw-r--r-- | background.js | 236 | ||||
-rw-r--r-- | inject-css.js | 101 | ||||
-rw-r--r-- | manifest.json | 14 | ||||
-rw-r--r-- | styling/README.md | 4 |
4 files changed, 312 insertions, 43 deletions
diff --git a/background.js b/background.js index 04adfd0..f823262 100644 --- a/background.js +++ b/background.js @@ -1,13 +1,173 @@ let SKIP_FORCE_THEMING_KEY = "skipForceThemingList"; let logging = false; +// Create a cache for pre-processed CSS to speed up repeated visits +const cssCache = new Map(); +const activeTabs = new Map(); + +// Preload styles for faster injection +async function preloadStyles() { + try { + const data = await browser.storage.local.get([ + "styles", + "transparentZenSettings", + ]); + const settings = data.transparentZenSettings || {}; + + // No point in preloading if styling is disabled + if (settings.enableStyling === false) return; + + // Clear the cache when reloaded to ensure fresh styles + cssCache.clear(); + + if (data.styles?.website) { + for (const [website, features] of Object.entries(data.styles.website)) { + // Process and store default CSS for each website (with all features enabled) + let combinedCSS = ""; + for (const [feature, css] of Object.entries(features)) { + combinedCSS += css + "\n"; + } + cssCache.set(website.replace(".css", ""), combinedCSS); + } + if (logging) console.log("Styles preloaded for faster injection"); + } + } catch (error) { + console.error("Error preloading styles:", error); + } +} + +// Handle web requests - allow injecting CSS before any content is loaded +browser.webNavigation.onBeforeNavigate.addListener((details) => { + if (details.frameId === 0) { + // Only for main frame + // Track active navigations + activeTabs.set(details.tabId, details.url); + + // Pre-fetch any styling needed for this URL + prepareStylesForUrl(new URL(details.url).hostname, details.tabId); + } +}); + +// Listen for content scripts announcing they're ready +browser.runtime.onMessage.addListener(async (message, sender) => { + if (message.action === "contentScriptReady" && message.hostname) { + try { + // Look for cached styles for this hostname or its domain match + const hostname = message.hostname; + + // Get settings to check if styling is enabled + const settingsData = await browser.storage.local.get( + "transparentZenSettings" + ); + const settings = settingsData.transparentZenSettings || {}; + + if (settings.enableStyling === false) return; + + const css = await getStylesForHostname(hostname, settings); + + // If we found matching CSS, send it immediately to the content script + if (css) { + browser.tabs + .sendMessage(sender.tab.id, { + action: "applyStyles", + css: css, + }) + .catch((err) => { + if (logging) console.log("Failed to send immediate CSS:", err); + }); + } + } catch (error) { + console.error("Error handling content script ready message:", error); + } + } else if (message.action === "enableAutoUpdate") { + startAutoUpdate(); + return true; + } else if (message.action === "disableAutoUpdate") { + stopAutoUpdate(); + return true; + } + + return false; +}); + +// Get appropriate styles for a hostname based on all rules +async function getStylesForHostname(hostname, settings) { + // Check for exact matches + if (cssCache.has(hostname)) { + return cssCache.get(hostname); + } else if (cssCache.has(`www.${hostname}`)) { + return cssCache.get(`www.${hostname}`); + } else { + // Check for wildcard matches (+domain.com) + for (const [cachedSite, cachedCSS] of cssCache.entries()) { + if (cachedSite.startsWith("+")) { + const baseSite = cachedSite.slice(1); + if (hostname.endsWith(baseSite)) { + return cachedCSS; + } + } else if (hostname.endsWith(cachedSite)) { + return cachedCSS; + } + } + + // Check for forced styles + if (settings.forceStyling) { + const skipListData = await browser.storage.local.get( + SKIP_FORCE_THEMING_KEY + ); + const siteList = skipListData[SKIP_FORCE_THEMING_KEY] || []; + const isWhitelistMode = settings.whitelistMode || false; + const siteInList = siteList.includes(hostname); + + // In whitelist mode: apply only if site is in the list + // In blacklist mode: apply only if site is NOT in the list + if ( + (isWhitelistMode && siteInList) || + (!isWhitelistMode && !siteInList) + ) { + if (cssCache.has("example.com")) { + return cssCache.get("example.com"); + } else { + return "/* Default fallback CSS */"; + } + } + } + } + + return null; +} + +// Prepare styles for a URL that's about to load +async function prepareStylesForUrl(hostname, tabId) { + try { + const settingsData = await browser.storage.local.get( + "transparentZenSettings" + ); + const settings = settingsData.transparentZenSettings || {}; + + if (settings.enableStyling === false) return; + + const css = await getStylesForHostname(hostname, settings); + + if (css && tabId) { + // Store the CSS to be ready as soon as the content script connects + activeTabs.set(tabId, { + hostname: hostname, + css: css, + }); + } + } catch (error) { + console.error("Error preparing styles for URL:", error); + } +} + async function applyCSSToTab(tab) { if (logging) console.log("applyCSSToTab called with", tab); - // Apply CSS to the specified tab - const url = new URL(tab.url); - const hostname = url.hostname; try { + const url = new URL(tab.url); + const hostname = url.hostname; + const settings = await browser.storage.local.get("transparentZenSettings"); const globalSettings = settings.transparentZenSettings || {}; if (globalSettings.enableStyling === false) return; @@ -71,7 +231,7 @@ async function applyCSSToTab(tab) { } } } catch (error) { - console.error(`Error applying CSS to ${hostname}:`, error); + console.error(`Error applying CSS:`, error); } } @@ -90,7 +250,19 @@ async function applyCSS(tabId, hostname, features) { } if (combinedCSS) { - await browser.tabs.insertCSS(tabId, { code: combinedCSS }); + try { + // Try to send via messaging (most reliable for instant application) + await browser.tabs.sendMessage(tabId, { + action: "applyStyles", + css: combinedCSS, + }); + } catch (e) { + // Fallback to insertCSS if messaging fails + await browser.tabs.insertCSS(tabId, { + code: combinedCSS, + runAt: "document_start", + }); + } console.log(`Injected custom CSS for ${hostname}`); } } @@ -99,66 +271,60 @@ let autoUpdateInterval; function startAutoUpdate() { if (logging) console.log("startAutoUpdate called"); - // Start the auto-update interval if (autoUpdateInterval) clearInterval(autoUpdateInterval); autoUpdateInterval = setInterval(refetchCSS, 2 * 60 * 60 * 1000); } function stopAutoUpdate() { if (logging) console.log("stopAutoUpdate called"); - // Stop the auto-update interval if (autoUpdateInterval) clearInterval(autoUpdateInterval); } async function refetchCSS() { if (logging) console.log("refetchCSS called"); - // Refetch CSS styles from the remote server try { const response = await fetch( "https://sameerasw.github.io/my-internet/styles.json", - { - headers: { "Cache-Control": "no-cache" }, - } + { headers: { "Cache-Control": "no-cache" } } ); if (!response.ok) throw new Error("Failed to fetch styles.json"); const styles = await response.json(); await browser.storage.local.set({ styles }); await browser.storage.local.set({ lastFetchedTime: Date.now() }); console.info("All styles refetched and updated from GitHub."); + + // Preload the new styles + preloadStyles(); } catch (error) { console.error("Error refetching styles:", error); } } -browser.runtime.onMessage.addListener((message) => { - if (logging) console.log("onMessage received", message); - // Handle messages for enabling/disabling auto-update - if (message.action === "enableAutoUpdate") { - startAutoUpdate(); - } else if (message.action === "disableAutoUpdate") { - stopAutoUpdate(); - } -}); +// Create a directory to store CSS files +async function initializeExtension() { + // Preload styles immediately + await preloadStyles(); -// Initialize auto-update based on stored settings -browser.storage.local.get("transparentZenSettings").then((settings) => { - if (logging) console.log("Initial settings loaded", settings); + // Initialize auto-update based on stored settings + const settings = await browser.storage.local.get("transparentZenSettings"); if (settings.transparentZenSettings?.autoUpdate) { startAutoUpdate(); } -}); +} -browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - // if (logging) console.log("onUpdated called with", tabId, changeInfo, tab); - // Apply CSS when a tab is updated - if (changeInfo.status === "complete" || changeInfo.status === "loading") { - applyCSSToTab(tab); +// Listen for specific navigation events to apply CSS as early as possible +browser.webNavigation.onCommitted.addListener((details) => { + if (details.frameId === 0) { + browser.tabs + .get(details.tabId) + .then((tab) => { + applyCSSToTab(tab); + }) + .catch((err) => { + console.error("Error getting tab info:", err); + }); } }); -browser.tabs.onActivated.addListener(async (activeInfo) => { - if (logging) console.log("onActivated called with", activeInfo); - // Apply CSS when a tab is activated - const tab = await browser.tabs.get(activeInfo.tabId); - applyCSSToTab(tab); -}); +// Application start +initializeExtension(); diff --git a/inject-css.js b/inject-css.js index d53a915..f37abd7 100644 --- a/inject-css.js +++ b/inject-css.js @@ -4,6 +4,91 @@ let logging = false; if (logging) console.log("inject-css.js script loaded"); +// Run as early as possible in the document lifecycle +const implementImmediateInjection = () => { + // Create a style element immediately to avoid any delay - do this before anything else + const styleElement = document.createElement("style"); + styleElement.id = "zen-internet-styles"; + + // Set highest priority + styleElement.setAttribute("data-priority", "highest"); + + // Add !important to all rules to override any existing styles + styleElement.innerHTML = ` + /* Prevent FOUC - temporarily hide content until styles are applied */ + body { opacity: 0 !important; transition: opacity 0.1s ease-in !important; } + `; + + // Insert as the first element of head if possible + if (document.head) { + document.head.insertBefore(styleElement, document.head.firstChild); + } else { + // If head isn't ready yet (very early execution), add to documentElement + document.documentElement.appendChild(styleElement); + + // Set up mutation observer to move it to head when head becomes available + new MutationObserver((mutations, observer) => { + if (document.head) { + if (styleElement.parentNode !== document.head) { + document.head.insertBefore(styleElement, document.head.firstChild); + } + observer.disconnect(); + } + }).observe(document.documentElement, { childList: true }); + } + + return styleElement; +}; + +// Create style element immediately +const styleElement = implementImmediateInjection(); + +// Function to apply styles immediately when available +function applyStyles(css) { + if (!css) return; + + // Add the CSS + try { + // For immediate application, directly set textContent + // as this is more reliably applied in early document stages + styleElement.textContent = css.trim() + ` +/* Remove FOUC prevention once styles are loaded */ +body { opacity: 1 !important; }`; + + // After a very short delay (to ensure CSS application), ensure body is visible + setTimeout(() => { + if (document.body) { + document.body.style.opacity = "1"; + } + }, 10); + + if (logging) console.log("Styles applied:", css.length, "bytes"); + } catch (e) { + console.error("Error applying styles:", e); + } +} + +// Listen for style data from background script for immediate injection +browser.runtime.onMessage.addListener((message) => { + if (message.action === "applyStyles" && message.css) { + applyStyles(message.css); + return true; + } +}); + +// Send hostname to background script as early as possible +browser.runtime + .sendMessage({ + action: "contentScriptReady", + hostname: window.location.hostname, + url: window.location.href, + }) + .catch((err) => { + if (logging) console.log("Background script not ready yet:", err); + }); + +// Main function - but we don't wait for this before applying styles +// This is just a backup in case background script injection fails (async () => { try { const settings = await browser.storage.local.get("transparentZenSettings"); @@ -15,7 +100,19 @@ if (logging) console.log("inject-css.js script loaded"); } if (logging) console.log("Styling is enabled"); + + // Tell background script we're ready and what page we're on + browser.runtime.sendMessage({ + action: "contentScriptReady", + hostname: window.location.hostname, + }); + const data = await browser.storage.local.get("styles"); + if (!data.styles) { + if (logging) console.log("No styles data available"); + return; + } + if (logging) console.log("Styles data loaded", data); const currentUrl = window.location.hostname; @@ -103,9 +200,7 @@ async function injectCSS(hostname, features) { } if (combinedCSS) { - const style = document.createElement("style"); - style.textContent = combinedCSS; - document.head.appendChild(style); + applyStyles(combinedCSS); if (logging) console.log(`Injected custom CSS for ${hostname}`); } } diff --git a/manifest.json b/manifest.json index 92a8578..3872bfc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Zen Internet", - "version": "1.5.0", + "version": "1.6.0", "description": "Inject custom css from my repository in real time", "browser_specific_settings": { "gecko": { @@ -17,7 +17,9 @@ "storage", "tabs", "<all_urls>", - "webNavigation" + "webNavigation", + "webRequest", + "webRequestBlocking" ], "browser_action": { "default_popup": "popup/popup.html", @@ -25,18 +27,20 @@ }, "background": { "scripts": ["background.js"], - "persistent": false + "persistent": true }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["inject-css.js"], - "run_at": "document_start" + "run_at": "document_start", + "all_frames": true } ], "web_accessible_resources": [ "data-viewer/data-viewer.html", "data-viewer/data-viewer.js", - "data-viewer/data-viewer.css" + "data-viewer/data-viewer.css", + "styling/*" ] } diff --git a/styling/README.md b/styling/README.md new file mode 100644 index 0000000..dc3847c --- /dev/null +++ b/styling/README.md @@ -0,0 +1,4 @@ +# Styling Directory + +This directory is used to store cached CSS files for faster injection. +The extension will generate and manage files in this directory automatically. |