Add files via upload

This commit is contained in:
Kenneth Hendricks 2025-03-17 22:11:43 -04:00 committed by GitHub
parent 37dba338af
commit 8171528bfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1876 additions and 959 deletions

File diff suppressed because it is too large Load diff

388
src/js/content.js Normal file
View file

@ -0,0 +1,388 @@
// FMHY SafeLink Guard - Content Script
// Implements visual marking of safe/unsafe links similar to the userscript
"use strict";
// Cross-browser compatibility shim
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
// Track processed elements to avoid reprocessing
const processedLinks = new WeakSet();
const processedDomains = new Set();
const highlightCountTrusted = new Map();
const highlightCountUntrusted = new Map();
// Default settings
let settings = {
highlightTrusted: true,
highlightUntrusted: true,
showWarningBanners: true,
trustedColor: "#32cd32",
untrustedColor: "#ff4444",
};
// Domain lists
let unsafeDomains = new Set();
let safeDomains = new Set();
let userTrusted = new Set();
let userUntrusted = new Set();
// Search engines where highlighting should be applied
const searchEngines = [
"google.com",
"google.", // Covers all Google country domains like google.co.uk, google.fr, etc.
"bing.com",
"duckduckgo.com",
"librey.org",
"4get.ca",
"mojeek.com",
"qwant.com",
"swisscows.com",
"yacy.net",
"startpage.com",
"search.brave.com",
"ekoru.org",
"gibiru.com",
"searx.org",
"searx.", // Covers all SearX instances
"searxng.", // Covers all SearXNG instances
"whoogle.", // Covers all Whoogle instances
"metager.org",
"ecosia.org",
"yandex.com",
"yandex.", // Covers all Yandex country domains
"yahoo.com",
"yahoo.", // Covers all Yahoo country domains
"baidu.com",
"naver.com",
"seznam.cz",
];
// FMHY domains to exclude
const fmhyDomains = [
"fmhy.net",
"fmhy.pages.dev",
"fmhy.lol",
"fmhy.vercel.app",
"fmhy.xyz",
];
// CSS for warning banners
const warningStyle = `
background-color: #ff0000;
color: #fff;
padding: 2px 6px;
font-weight: bold;
border-radius: 4px;
font-size: 12px;
margin-left: 6px;
z-index: 9999;
`;
// Main initialization
function init() {
loadSettings()
.then(() => loadDomainLists())
.then(() => {
processPage();
setupObserver();
})
.catch((err) =>
console.error("[FMHY SafeGuard] Error initializing content script:", err)
);
}
// Check if current site is a search engine where we should apply highlighting
function isSupportedSite(domain) {
// Don't highlight on FMHY sites
if (fmhyDomains.some((fmhyDomain) => domain.endsWith(fmhyDomain))) {
console.log(
`[FMHY SafeGuard] Skipping highlighting on FMHY domain: ${domain}`
);
return false;
}
// Only highlight on search engines
return searchEngines.some((searchDomain) => domain.includes(searchDomain));
}
// Load user settings from storage
async function loadSettings() {
try {
const data = await browserAPI.storage.local.get([
"highlightTrusted",
"highlightUntrusted",
"showWarningBanners",
"trustedColor",
"untrustedColor",
"userTrustedDomains",
"userUntrustedDomains",
]);
// Apply stored settings or use defaults
settings.highlightTrusted =
data.highlightTrusted !== undefined
? data.highlightTrusted
: settings.highlightTrusted;
settings.highlightUntrusted =
data.highlightUntrusted !== undefined
? data.highlightUntrusted
: settings.highlightUntrusted;
settings.showWarningBanners =
data.showWarningBanners !== undefined
? data.showWarningBanners
: settings.showWarningBanners;
if (data.trustedColor) settings.trustedColor = data.trustedColor;
if (data.untrustedColor) settings.untrustedColor = data.untrustedColor;
// Load user trusted/untrusted domains
if (data.userTrustedDomains) {
userTrusted = new Set(data.userTrustedDomains);
}
if (data.userUntrustedDomains) {
userUntrusted = new Set(data.userUntrustedDomains);
}
console.log("[FMHY SafeGuard] Settings loaded:", settings);
} catch (error) {
console.error("[FMHY SafeGuard] Error loading settings:", error);
}
}
// Load domain lists from extension storage
async function loadDomainLists() {
try {
const { unsafeSites, safeSiteList } = await browserAPI.storage.local.get([
"unsafeSites",
"safeSiteList",
]);
if (unsafeSites && Array.isArray(unsafeSites)) {
unsafeDomains = new Set(
unsafeSites.map((site) => normalizeDomain(new URL(site).hostname))
);
}
if (safeSiteList && Array.isArray(safeSiteList)) {
safeDomains = new Set(
safeSiteList.map((site) => normalizeDomain(new URL(site).hostname))
);
}
// Apply user overrides
applyUserOverrides();
console.log(
`[FMHY SafeGuard] Loaded ${unsafeDomains.size} unsafe domains and ${safeDomains.size} safe domains`
);
} catch (error) {
console.error("[FMHY SafeGuard] Error loading domain lists:", error);
}
}
// Apply user trusted/untrusted overrides
function applyUserOverrides() {
userTrusted.forEach((domain) => {
safeDomains.add(domain);
unsafeDomains.delete(domain);
});
userUntrusted.forEach((domain) => {
unsafeDomains.add(domain);
safeDomains.delete(domain);
});
}
// Process all links in the page
function processPage() {
const currentDomain = normalizeDomain(window.location.hostname);
// Only process links on search engines and not on FMHY sites
if (!isSupportedSite(currentDomain)) {
console.log(
`[FMHY SafeGuard] Skipping highlighting on non-search engine: ${currentDomain}`
);
return;
}
console.log(
`[FMHY SafeGuard] Processing links on search engine: ${currentDomain}`
);
document
.querySelectorAll("a[href]")
.forEach((link) => processLink(link, currentDomain));
}
// Set up mutation observer to handle dynamically added content
function setupObserver() {
const currentDomain = normalizeDomain(window.location.hostname);
// Skip setting up observer if not on a supported site
if (!isSupportedSite(currentDomain)) {
return;
}
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
// If it's a link itself
if (node.tagName === "A" && node.href) {
processLink(node, currentDomain);
}
// Process any links inside the added node
node
.querySelectorAll("a[href]")
.forEach((link) => processLink(link, currentDomain));
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// Process a single link
function processLink(link, currentDomain) {
// Skip if already processed
if (processedLinks.has(link)) return;
processedLinks.add(link);
try {
// Skip links without proper URLs
if (
!link.href ||
link.href.startsWith("javascript:") ||
link.href.startsWith("#")
) {
return;
}
const linkDomain = normalizeDomain(new URL(link.href).hostname);
// Skip if the current site is safe AND the link is internal
if (
(safeDomains.has(currentDomain) || userTrusted.has(currentDomain)) &&
linkDomain === currentDomain
) {
return;
}
// Handle untrusted links
if (
userUntrusted.has(linkDomain) ||
(!userTrusted.has(linkDomain) && unsafeDomains.has(linkDomain))
) {
if (
settings.highlightUntrusted &&
getHighlightCount(highlightCountUntrusted, linkDomain) < 2
) {
highlightLink(link, "untrusted");
incrementHighlightCount(highlightCountUntrusted, linkDomain);
}
if (settings.showWarningBanners && !processedDomains.has(linkDomain)) {
addWarningBanner(link);
processedDomains.add(linkDomain);
}
}
// Handle trusted links
else if (userTrusted.has(linkDomain) || safeDomains.has(linkDomain)) {
if (
settings.highlightTrusted &&
getHighlightCount(highlightCountTrusted, linkDomain) < 2
) {
highlightLink(link, "trusted");
incrementHighlightCount(highlightCountTrusted, linkDomain);
}
}
} catch (error) {
console.warn("[FMHY SafeGuard] Error processing link:", error);
}
}
// Highlight a link based on its trustworthiness
function highlightLink(link, type) {
const color =
type === "trusted" ? settings.trustedColor : settings.untrustedColor;
link.style.textShadow = `0 0 4px ${color}`;
link.style.fontWeight = "bold";
}
// Add a warning banner after an unsafe link
function addWarningBanner(link) {
const warning = document.createElement("span");
warning.textContent = "⚠️ FMHY Unsafe Site";
warning.style = warningStyle;
link.after(warning);
}
// Helper functions
function normalizeDomain(hostname) {
return hostname.replace(/^www\./, "").toLowerCase();
}
function getHighlightCount(map, domain) {
return map.get(domain) || 0;
}
function incrementHighlightCount(map, domain) {
if (map.size > 1000) map.clear(); // Reset if too large
map.set(domain, getHighlightCount(map, domain) + 1);
}
// Listen for settings changes
browserAPI.storage.onChanged.addListener((changes, area) => {
if (area === "local") {
let settingsChanged = false;
for (let key in changes) {
if (
key === "highlightTrusted" ||
key === "highlightUntrusted" ||
key === "showWarningBanners" ||
key === "trustedColor" ||
key === "untrustedColor" ||
key === "userTrustedDomains" ||
key === "userUntrustedDomains"
) {
settingsChanged = true;
}
}
if (settingsChanged) {
refreshPage();
}
}
});
// Handle messages from background script
browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "refreshSettings") {
refreshPage();
sendResponse({ status: "success" });
return true;
}
return false;
});
// Function to refresh page highlighting
function refreshPage() {
// Reload settings and reprocess the page
loadSettings()
.then(() => loadDomainLists())
.then(() => {
// Clear processed state and reprocess
processedLinks.clear();
processedDomains.clear();
highlightCountTrusted.clear();
highlightCountUntrusted.clear();
processPage();
});
}
// Start the script
init();

View file

@ -1,152 +1,152 @@
document.addEventListener("DOMContentLoaded", async () => {
console.log("Popup loaded, preparing to check site status...");
const statusIcon = document.getElementById("status-icon");
const statusMessage = document.getElementById("status-message");
const errorMessage = document.getElementById("error-message");
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
const warningPageUrl = browserAPI.runtime.getURL("pub/warning-page.html");
const settingsPageUrl = browserAPI.runtime.getURL("pub/settings-page.html");
const welcomePageUrl = browserAPI.runtime.getURL("pub/welcome-page.html");
async function applyTheme() {
try {
const { theme } = await browserAPI.storage.sync.get("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
document.body.setAttribute(
"data-theme",
theme || (prefersDark ? "dark" : "light")
);
} catch (error) {
console.error("Error applying theme:", error);
}
}
await applyTheme();
try {
const [activeTab] = await browserAPI.tabs.query({
active: true,
currentWindow: true,
});
if (!activeTab || !activeTab.url) {
throw new Error("No active tab found or URL is unavailable.");
}
const currentUrl = activeTab.url;
const rootUrl = extractRootUrl(currentUrl);
if (
currentUrl.startsWith(warningPageUrl) ||
currentUrl === settingsPageUrl ||
currentUrl === welcomePageUrl
) {
handleStatusUpdate("extension_page", currentUrl);
return;
}
// Send both the full URL and root URL to the background for status checking
const response = await browserAPI.runtime.sendMessage({
action: "checkSiteStatus",
url: currentUrl, // full path URL
rootUrl: rootUrl, // root domain URL
});
if (!response || !response.status) {
throw new Error(
"Failed to retrieve site status from the background script."
);
}
// Display the appropriate URL in the popup
const displayUrl = response.matchedUrl || rootUrl;
handleStatusUpdate(response.status, displayUrl);
} catch (error) {
console.error("Error while checking site status:", error);
errorMessage.textContent = `Error: ${error.message}`;
updateUI("error", "An error occurred while retrieving the site status.");
}
function handleStatusUpdate(status, displayUrl) {
let message;
switch (status) {
case "unsafe":
message = `${displayUrl} is flagged as <strong>unsafe</strong>. Be cautious when interacting with this site.`;
break;
case "potentially_unsafe":
message = `${displayUrl} is <strong>potentially unsafe</strong>. Proceed with caution.`;
break;
case "fmhy":
message = `${displayUrl} is an <strong>FMHY</strong> related site. Proceed confidently.`;
break;
case "safe":
message = `${displayUrl} is <strong>safe</strong> to browse.`;
break;
case "starred":
message = `${displayUrl} is a <strong>starred</strong> site.`;
break;
case "extension_page":
if (displayUrl.startsWith(warningPageUrl)) {
message =
"You are on the <strong>Warning Page</strong>. This page warns you about potentially unsafe sites.";
} else if (displayUrl === settingsPageUrl) {
message =
"This is the <strong>Settings Page</strong> of the extension. Customize your preferences here.";
} else if (displayUrl === welcomePageUrl) {
message =
"Welcome to <strong>FMHY SafeGuard</strong>! Explore the extension's features and get started.";
} else {
message = "This is an <strong>extension page</strong>.";
}
break;
case "no_data":
message = `No data available for <strong>${displayUrl}</strong>.`;
break;
default:
message = `An unknown status was received for <strong>${displayUrl}</strong>.`;
}
updateUI(status, message);
}
function updateUI(status, message) {
const icons = {
unsafe: "../res/icons/unsafe.png",
potentially_unsafe: "../res/icons/potentially_unsafe.png",
fmhy: "../res/icons/fmhy.png",
safe: "../res/icons/safe.png",
starred: "../res/icons/starred.png",
extension_page: "../res/ext_icon_144.png",
no_data: "../res/ext_icon_144.png",
error: "../res/icons/error.png",
unknown: "../res/ext_icon_144.png",
};
statusIcon.src = icons[status] || icons["unknown"];
statusMessage.innerHTML = message || "An unknown error occurred.";
statusIcon.classList.add("active");
setTimeout(() => statusIcon.classList.remove("active"), 300);
console.log(`UI updated: ${message}`);
}
document.getElementById("settingsButton").addEventListener("click", () => {
browserAPI.runtime.openOptionsPage();
});
function extractRootUrl(url) {
try {
const urlObj = new URL(url);
return `${urlObj.protocol}//${urlObj.hostname}`;
} catch (error) {
console.warn(`Failed to extract root URL from: ${url}`);
return url;
}
}
});
document.addEventListener("DOMContentLoaded", async () => {
console.log("Popup loaded, preparing to check site status...");
const statusIcon = document.getElementById("status-icon");
const statusMessage = document.getElementById("status-message");
const errorMessage = document.getElementById("error-message");
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
const warningPageUrl = browserAPI.runtime.getURL("pub/warning-page.html");
const settingsPageUrl = browserAPI.runtime.getURL("pub/settings-page.html");
const welcomePageUrl = browserAPI.runtime.getURL("pub/welcome-page.html");
async function applyTheme() {
try {
const { theme } = await browserAPI.storage.sync.get("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
document.body.setAttribute(
"data-theme",
theme || (prefersDark ? "dark" : "light")
);
} catch (error) {
console.error("Error applying theme:", error);
}
}
await applyTheme();
try {
const [activeTab] = await browserAPI.tabs.query({
active: true,
currentWindow: true,
});
if (!activeTab || !activeTab.url) {
throw new Error("No active tab found or URL is unavailable.");
}
const currentUrl = activeTab.url;
const rootUrl = extractRootUrl(currentUrl);
if (
currentUrl.startsWith(warningPageUrl) ||
currentUrl === settingsPageUrl ||
currentUrl === welcomePageUrl
) {
handleStatusUpdate("extension_page", currentUrl);
return;
}
// Send both the full URL and root URL to the background for status checking
const response = await browserAPI.runtime.sendMessage({
action: "checkSiteStatus",
url: currentUrl, // full path URL
rootUrl: rootUrl, // root domain URL
});
if (!response || !response.status) {
throw new Error(
"Failed to retrieve site status from the background script."
);
}
// Display the appropriate URL in the popup
const displayUrl = response.matchedUrl || rootUrl;
handleStatusUpdate(response.status, displayUrl);
} catch (error) {
console.error("Error while checking site status:", error);
errorMessage.textContent = `Error: ${error.message}`;
updateUI("error", "An error occurred while retrieving the site status.");
}
function handleStatusUpdate(status, displayUrl) {
let message;
switch (status) {
case "unsafe":
message = `${displayUrl} is flagged as <strong>unsafe</strong>. Be cautious when interacting with this site.`;
break;
case "potentially_unsafe":
message = `${displayUrl} is <strong>potentially unsafe</strong>. Proceed with caution.`;
break;
case "fmhy":
message = `${displayUrl} is an <strong>FMHY</strong> related site. Proceed confidently.`;
break;
case "safe":
message = `${displayUrl} is <strong>safe</strong> to browse.`;
break;
case "starred":
message = `${displayUrl} is a <strong>starred</strong> site.`;
break;
case "extension_page":
if (displayUrl.startsWith(warningPageUrl)) {
message =
"You are on the <strong>Warning Page</strong>. This page warns you about potentially unsafe sites.";
} else if (displayUrl === settingsPageUrl) {
message =
"This is the <strong>Settings Page</strong> of the extension. Customize your preferences here.";
} else if (displayUrl === welcomePageUrl) {
message =
"Welcome to <strong>FMHY SafeGuard</strong>! Explore the extension's features and get started.";
} else {
message = "This is an <strong>extension page</strong>.";
}
break;
case "no_data":
message = `No data available for <strong>${displayUrl}</strong>.`;
break;
default:
message = `An unknown status was received for <strong>${displayUrl}</strong>.`;
}
updateUI(status, message);
}
function updateUI(status, message) {
const icons = {
unsafe: "../res/icons/unsafe.png",
potentially_unsafe: "../res/icons/potentially_unsafe.png",
fmhy: "../res/icons/fmhy.png",
safe: "../res/icons/safe.png",
starred: "../res/icons/starred.png",
extension_page: "../res/ext_icon_144.png",
no_data: "../res/ext_icon_144.png",
error: "../res/icons/error.png",
unknown: "../res/ext_icon_144.png",
};
statusIcon.src = icons[status] || icons["unknown"];
statusMessage.innerHTML = message || "An unknown error occurred.";
statusIcon.classList.add("active");
setTimeout(() => statusIcon.classList.remove("active"), 300);
console.log(`UI updated: ${message}`);
}
document.getElementById("settingsButton").addEventListener("click", () => {
browserAPI.runtime.openOptionsPage();
});
function extractRootUrl(url) {
try {
const urlObj = new URL(url);
return `${urlObj.protocol}//${urlObj.hostname}`;
} catch (error) {
console.warn(`Failed to extract root URL from: ${url}`);
return url;
}
}
});

View file

@ -520,6 +520,9 @@
</div>
<div class="update-status" id="updateStatus">
<svg class="update-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 4V2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12H20C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"/>
</svg>
Next update scheduled for: Checking...
</div>
</div>
@ -559,6 +562,68 @@
</div>
</div>
<!-- Link Highlighting Settings -->
<div class="settings-section">
<h2 class="section-title">Link Highlighting</h2>
<p class="section-description">
Visually highlight safe and unsafe links in web pages
</p>
<div class="settings-grid" style="margin-top: 10px;">
<div>
<p class="section-description" style="margin: 0;">
Highlight safe links (green)
</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="highlightTrustedToggle" checked />
<span class="toggle-slider"></span>
</label>
</div>
<div class="settings-grid" style="margin-top: 15px;">
<div>
<p class="section-description" style="margin: 0;">
Highlight unsafe links (red)
</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="highlightUntrustedToggle" checked />
<span class="toggle-slider"></span>
</label>
</div>
<div class="settings-grid" style="margin-top: 15px;">
<div>
<p class="section-description" style="margin: 0;">
Show warning banners next to unsafe links
</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="showWarningBannersToggle" checked />
<span class="toggle-slider"></span>
</label>
</div>
<div class="settings-grid" style="margin-top: 15px;">
<div>
<p class="section-description" style="margin: 0;">
Safe link color
</p>
</div>
<input type="color" id="trustedColor" value="#32cd32" style="width: 50px; height: 30px; border: none; border-radius: 4px;">
</div>
<div class="settings-grid" style="margin-top: 15px;">
<div>
<p class="section-description" style="margin: 0;">
Unsafe link color
</p>
</div>
<input type="color" id="untrustedColor" value="#ff4444" style="width: 50px; height: 30px; border: none; border-radius: 4px;">
</div>
</div>
<!-- Auto-Update Settings -->
<div class="settings-section">
<div class="settings-grid">
@ -577,13 +642,48 @@
</div>
</div>
</div>
<!-- Domain Management -->
<div class="settings-section">
<h2 class="section-title">Domain Management</h2>
<p class="section-description">
Manually add domains to safe or unsafe lists
</p>
<div style="display: flex; gap: 20px; margin-top: 15px;">
<div style="flex: 1;">
<p class="section-description" style="margin-bottom: 5px; font-weight: bold;">
Safe Domains (one per line)
</p>
<textarea id="trustedDomains" rows="6" style="width: 100%; background: rgba(255, 255, 255, 0.05); color: var(--text-primary); border: 1px solid var(--section-border); border-radius: 8px; padding: 8px; font-family: monospace; font-size: 14px;"></textarea>
</div>
<div style="flex: 1;">
<p class="section-description" style="margin-bottom: 5px; font-weight: bold;">
Unsafe Domains (one per line)
</p>
<textarea id="untrustedDomains" rows="6" style="width: 100%; background: rgba(255, 255, 255, 0.05); color: var(--text-primary); border: 1px solid var(--section-border); border-radius: 8px; padding: 8px; font-family: monospace; font-size: 14px;"></textarea>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div style="text-align: center">
<button class="btn" id="saveSettings">Save Settings</button>
<!-- Save Button Section -->
<div style="text-align: center; margin-top: 20px;">
<button
id="saveSettings"
class="btn"
>
Save Settings
</button>
</div>
<div
id="notification"
class="notification"
style="display: none"
></div>
<div class="footer">
<p>FMHY SafeGuard <span id="versionNumber"></p>
<p>
@ -595,11 +695,6 @@
</div>
</div>
<!-- Notification Toast -->
<div class="notification" id="notification">
Settings saved successfully!
</div>
<script src="settings-page.js"></script>
</body>
</html>

View file

@ -1,273 +1,423 @@
document.addEventListener("DOMContentLoaded", () => {
// Cross-browser compatibility shim
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
// Load and display the extension version from manifest.json
const manifest = browserAPI.runtime.getManifest();
document.getElementById("versionNumber").textContent = manifest.version;
// Get all DOM elements
const themeSelect = document.getElementById("themeSelect");
const warningToggle = document.getElementById("warningToggle");
const updateFrequency = document.getElementById("updateFrequency");
const saveButton = document.getElementById("saveSettings");
const notification = document.getElementById("notification");
const lastUpdated = document.getElementById("lastUpdated");
const updateStatus = document.getElementById("updateStatus");
// Theme application function
function applyTheme(theme) {
if (theme === "system") {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
document.body.setAttribute("data-theme", prefersDark ? "dark" : "light");
} else {
document.body.setAttribute("data-theme", theme);
}
}
// Format date function
function formatDate(date) {
if (!date) return "Never";
const d = new Date(date);
const now = new Date();
const diffTime = Math.abs(now - d);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return (
"Today at " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
} else if (diffDays === 1) {
return (
"Yesterday at " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
} else {
return (
d.toLocaleDateString() +
" " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
}
}
// Calculate next update time
function calculateNextUpdate(lastUpdate, frequency) {
if (!lastUpdate) return "Not scheduled";
const lastUpdateDate = new Date(lastUpdate);
let nextUpdate = new Date(lastUpdateDate);
switch (frequency) {
case "daily":
nextUpdate.setDate(nextUpdate.getDate() + 1);
nextUpdate.setHours(0, 0, 0, 0);
break;
case "weekly":
nextUpdate.setDate(nextUpdate.getDate() + 7);
nextUpdate.setHours(0, 0, 0, 0);
break;
case "monthly":
nextUpdate.setMonth(nextUpdate.getMonth() + 1);
nextUpdate.setHours(0, 0, 0, 0);
break;
default:
return "Not scheduled";
}
const now = new Date();
if (nextUpdate < now) {
return "Update pending...";
}
const timeUntil = nextUpdate - now;
const hoursUntil = Math.floor(timeUntil / (1000 * 60 * 60));
const minutesUntil = Math.floor(
(timeUntil % (1000 * 60 * 60)) / (1000 * 60)
);
if (hoursUntil < 24) {
if (hoursUntil === 0) {
return `in ${minutesUntil} minutes`;
} else {
return `in ${hoursUntil}h ${minutesUntil}m`;
}
} else {
const days = Math.floor(hoursUntil / 24);
if (days === 1) {
return "tomorrow";
} else {
return `in ${days} days`;
}
}
}
// Update the UI with next update time
async function updateNextUpdateStatus() {
try {
const stats = await browserAPI.storage.local.get({
lastUpdated: null,
});
const settings = await browserAPI.storage.sync.get({
updateFrequency: "daily",
});
const nextUpdateText = calculateNextUpdate(
stats.lastUpdated,
settings.updateFrequency
);
if (updateStatus) {
updateStatus.innerHTML = `
<svg class="update-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 4V2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12H20C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"/>
</svg>
Next update ${nextUpdateText}
`;
}
} catch (error) {
console.error("Error updating next update status:", error);
if (updateStatus) {
updateStatus.textContent = "Unable to check next update time";
}
}
}
// Load filterlist stats
async function loadFilterlistStats() {
try {
const stats = await browserAPI.storage.local.get({
unsafeFilterCount: 0,
potentiallyUnsafeFilterCount: 0,
safeSiteCount: 0,
lastUpdated: null,
});
console.log("Fetched stats:", stats);
document.getElementById("unsafeFilterCount").textContent =
stats.unsafeFilterCount;
document.getElementById("potentiallyUnsafeFilterCount").textContent =
stats.potentiallyUnsafeFilterCount;
document.getElementById("safeSiteCount").textContent =
stats.safeSiteCount;
document.getElementById("lastUpdated").textContent = formatDate(
stats.lastUpdated
);
await updateNextUpdateStatus();
} catch (error) {
console.error("Error loading filterlist stats:", error);
document.getElementById("unsafeFilterCount").textContent = "Error";
document.getElementById("potentiallyUnsafeFilterCount").textContent =
"Error";
document.getElementById("safeSiteCount").textContent = "Error";
document.getElementById("lastUpdated").textContent = "Error";
}
}
// Load settings function
async function loadSettings() {
try {
const savedSettings = await browserAPI.storage.sync.get({
theme: "system",
warningPage: true,
updateFrequency: "daily",
});
if (themeSelect) themeSelect.value = savedSettings.theme;
if (warningToggle) warningToggle.checked = savedSettings.warningPage;
if (updateFrequency)
updateFrequency.value = savedSettings.updateFrequency;
applyTheme(savedSettings.theme);
await loadFilterlistStats();
} catch (error) {
console.error("Error loading settings:", error);
showNotification("Error loading settings", true);
}
}
// Show notification function
function showNotification(message, isError = false) {
if (notification) {
notification.textContent = message;
if (isError) {
notification.style.background =
"linear-gradient(120deg, #ff6b6b, #ff8787)";
} else {
notification.style.background =
"linear-gradient(120deg, var(--accent-purple), var(--accent-blue))";
}
notification.classList.add("show");
setTimeout(() => {
notification.classList.remove("show");
}, 3000);
}
}
// Save settings function
async function saveSettings() {
try {
const settings = {
theme: themeSelect.value,
warningPage: warningToggle.checked,
updateFrequency: updateFrequency.value,
};
console.log("Saving settings:", settings);
// Save settings to storage
await browserAPI.storage.sync.set(settings);
showNotification("Settings saved successfully!");
applyTheme(settings.theme);
await updateNextUpdateStatus();
console.log(
"Settings saved to storage, sending message to background script..."
);
// Send updated settings to background script
await browserAPI.runtime.sendMessage({
type: "settingsUpdated",
settings: settings,
});
console.log(
"Settings update message sent to background script successfully."
);
} catch (error) {
console.error("Error occurred during saveSettings:", error);
showNotification("Error saving settings", true);
}
}
// Event Listeners
if (saveButton) {
saveButton.addEventListener("click", saveSettings);
}
if (themeSelect) {
themeSelect.addEventListener("change", (e) => {
applyTheme(e.target.value);
});
}
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (themeSelect && themeSelect.value === "system") {
applyTheme("system");
}
});
browserAPI.runtime.onMessage.addListener((message) => {
if (message.type === "filterlistUpdated") {
loadFilterlistStats();
}
});
setInterval(updateNextUpdateStatus, 60000);
loadSettings();
});
document.addEventListener("DOMContentLoaded", () => {
// Cross-browser compatibility shim
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
// Load and display the extension version from manifest.json
const manifest = browserAPI.runtime.getManifest();
document.getElementById("versionNumber").textContent = manifest.version;
// Get all DOM elements
const themeSelect = document.getElementById("themeSelect");
const warningToggle = document.getElementById("warningToggle");
const updateFrequency = document.getElementById("updateFrequency");
const saveButton = document.getElementById("saveSettings");
const notification = document.getElementById("notification");
const lastUpdated = document.getElementById("lastUpdated");
const updateStatus = document.getElementById("updateStatus");
const forceRefreshButton = document.getElementById("forceRefresh");
// Get link highlighting elements
const highlightTrustedToggle = document.getElementById(
"highlightTrustedToggle"
);
const highlightUntrustedToggle = document.getElementById(
"highlightUntrustedToggle"
);
const showWarningBannersToggle = document.getElementById(
"showWarningBannersToggle"
);
const trustedColor = document.getElementById("trustedColor");
const untrustedColor = document.getElementById("untrustedColor");
// Get domain management elements
const trustedDomains = document.getElementById("trustedDomains");
const untrustedDomains = document.getElementById("untrustedDomains");
// Theme application function
function applyTheme(theme) {
if (theme === "system") {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
document.body.setAttribute("data-theme", prefersDark ? "dark" : "light");
} else {
document.body.setAttribute("data-theme", theme);
}
}
// Format date function
function formatDate(date) {
if (!date) return "Never";
const d = new Date(date);
const now = new Date();
const diffTime = Math.abs(now - d);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return (
"Today at " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
} else if (diffDays === 1) {
return (
"Yesterday at " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
} else {
return (
d.toLocaleDateString() +
" " +
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
);
}
}
// Calculate next update time
function calculateNextUpdate(lastUpdate, frequency) {
if (!lastUpdate) return "Not scheduled";
const lastUpdateDate = new Date(lastUpdate);
let nextUpdate = new Date(lastUpdateDate);
switch (frequency) {
case "daily":
nextUpdate.setDate(nextUpdate.getDate() + 1);
nextUpdate.setHours(0, 0, 0, 0);
break;
case "weekly":
nextUpdate.setDate(nextUpdate.getDate() + 7);
nextUpdate.setHours(0, 0, 0, 0);
break;
case "monthly":
nextUpdate.setMonth(nextUpdate.getMonth() + 1);
nextUpdate.setHours(0, 0, 0, 0);
break;
default:
return "Not scheduled";
}
const now = new Date();
if (nextUpdate < now) {
return "Update pending...";
}
const timeUntil = nextUpdate - now;
const hoursUntil = Math.floor(timeUntil / (1000 * 60 * 60));
const minutesUntil = Math.floor(
(timeUntil % (1000 * 60 * 60)) / (1000 * 60)
);
if (hoursUntil < 24) {
if (hoursUntil === 0) {
return `in ${minutesUntil} minutes`;
} else {
return `in ${hoursUntil}h ${minutesUntil}m`;
}
} else {
const days = Math.floor(hoursUntil / 24);
if (days === 1) {
return "tomorrow";
} else {
return `in ${days} days`;
}
}
}
// Update the UI with next update time
async function updateNextUpdateStatus() {
try {
// Get both lastUpdated and updateFrequency in a single storage call
const data = await browserAPI.storage.local.get({
lastUpdated: null,
updateFrequency: "daily",
});
// Use the stored frequency setting directly
const nextUpdateText = calculateNextUpdate(
data.lastUpdated,
data.updateFrequency
);
if (updateStatus) {
// Clear current content
while (updateStatus.firstChild) {
updateStatus.removeChild(updateStatus.firstChild);
}
// Add spinning icon
const svgIcon = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
svgIcon.setAttribute("class", "update-icon");
svgIcon.setAttribute("viewBox", "0 0 24 24");
svgIcon.setAttribute("width", "16");
svgIcon.setAttribute("height", "16");
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.setAttribute("fill", "currentColor");
path.setAttribute(
"d",
"M12 4V2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12H20C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"
);
svgIcon.appendChild(path);
updateStatus.appendChild(svgIcon);
// Add text
updateStatus.appendChild(
document.createTextNode(`Next update ${nextUpdateText}`)
);
}
} catch (error) {
console.error("Error updating next update status:", error);
if (updateStatus) {
updateStatus.textContent = "Unable to check next update time";
}
}
}
// Load filterlist stats
async function loadFilterlistStats() {
try {
const stats = await browserAPI.storage.local.get({
unsafeFilterCount: 0,
potentiallyUnsafeFilterCount: 0,
safeSiteCount: 0,
lastUpdated: null,
});
console.log("Fetched stats:", stats);
document.getElementById("unsafeFilterCount").textContent =
stats.unsafeFilterCount;
document.getElementById("potentiallyUnsafeFilterCount").textContent =
stats.potentiallyUnsafeFilterCount;
document.getElementById("safeSiteCount").textContent =
stats.safeSiteCount;
document.getElementById("lastUpdated").textContent = formatDate(
stats.lastUpdated
);
await updateNextUpdateStatus();
} catch (error) {
console.error("Error loading filterlist stats:", error);
document.getElementById("unsafeFilterCount").textContent = "Error";
document.getElementById("potentiallyUnsafeFilterCount").textContent =
"Error";
document.getElementById("safeSiteCount").textContent = "Error";
document.getElementById("lastUpdated").textContent = "Error";
}
}
// Load settings function
async function loadSettings() {
try {
const settings = await browserAPI.storage.local.get([
"theme",
"showWarning",
"updateFrequency",
"lastUpdated",
"nextUpdate",
"highlightTrusted",
"highlightUntrusted",
"showWarningBanners",
"trustedColor",
"untrustedColor",
"userTrustedDomains",
"userUntrustedDomains",
]);
console.log("Loaded settings:", settings);
themeSelect.value = settings.theme || "system";
applyTheme(settings.theme || "system");
warningToggle.checked = settings.showWarning !== false;
// Set updateFrequency with fallback to "daily"
updateFrequency.value = settings.updateFrequency || "daily";
console.log("Set updateFrequency to:", updateFrequency.value);
// Set link highlighting settings
highlightTrustedToggle.checked = settings.highlightTrusted !== false;
highlightUntrustedToggle.checked = settings.highlightUntrusted !== false;
showWarningBannersToggle.checked = settings.showWarningBanners !== false;
if (settings.trustedColor) {
trustedColor.value = settings.trustedColor;
}
if (settings.untrustedColor) {
untrustedColor.value = settings.untrustedColor;
}
// Set domain lists
if (
settings.userTrustedDomains &&
Array.isArray(settings.userTrustedDomains)
) {
trustedDomains.value = settings.userTrustedDomains.join("\n");
}
if (
settings.userUntrustedDomains &&
Array.isArray(settings.userUntrustedDomains)
) {
untrustedDomains.value = settings.userUntrustedDomains.join("\n");
}
if (settings.lastUpdated) {
lastUpdated.textContent = formatDate(settings.lastUpdated);
}
if (settings.nextUpdate) {
updateNextUpdateStatus();
}
} catch (error) {
console.error("Error loading settings:", error);
showNotification("Error loading settings", true);
}
}
// Show notification function
function showNotification(message, isError = false) {
if (notification) {
notification.textContent = message;
if (isError) {
notification.style.background =
"linear-gradient(120deg, #ff6b6b, #ff8787)";
} else {
notification.style.background =
"linear-gradient(120deg, var(--accent-purple), var(--accent-blue))";
}
notification.classList.add("show");
setTimeout(() => {
notification.classList.remove("show");
}, 3000);
}
}
/**
* Parse domains from textarea
* @param {string} text - Textarea content
* @returns {string[]} - Array of normalized domains
*/
function parseDomainList(text) {
if (!text) return [];
return text
.split("\n")
.map((line) => line.trim().toLowerCase())
.filter((line) => line && !line.startsWith("#") && !line.startsWith("//"))
.map((domain) => {
// Remove protocols and paths
if (domain.includes("://")) {
try {
return new URL(domain).hostname.replace(/^www\./, "");
} catch (e) {
return domain;
}
}
// Just remove www prefix if no protocol
return domain.replace(/^www\./, "");
});
}
// Save settings function
async function saveSettings() {
try {
const newSettings = {
theme: themeSelect.value,
showWarning: warningToggle.checked,
updateFrequency: updateFrequency.value,
highlightTrusted: highlightTrustedToggle.checked,
highlightUntrusted: highlightUntrustedToggle.checked,
showWarningBanners: showWarningBannersToggle.checked,
trustedColor: trustedColor.value,
untrustedColor: untrustedColor.value,
userTrustedDomains: parseDomainList(trustedDomains.value),
userUntrustedDomains: parseDomainList(untrustedDomains.value),
};
// Save all settings explicitly to local storage
await browserAPI.storage.local.set(newSettings);
// Calculate the next update time based on the new frequency
const nextUpdate = calculateNextUpdate(
new Date().toISOString(),
newSettings.updateFrequency
);
// Update lastUpdated to now and store the next update time
await browserAPI.storage.local.set({
nextUpdate,
lastUpdated: new Date().toISOString(),
});
// Tell the background script to update its alarm
await browserAPI.runtime.sendMessage({ action: "updateAlarm" });
// Apply settings to all open tabs
await browserAPI.runtime.sendMessage({ action: "refreshAllTabs" });
// Show notification and update status
showNotification("Settings saved and applied to all tabs!");
await updateNextUpdateStatus();
} catch (error) {
console.error("Error saving settings:", error);
showNotification("Error saving settings. Please try again.", true);
}
}
// For debugging - can be called from the browser console
window.checkCurrentSettings = async function () {
try {
const data = await browserAPI.storage.local.get(null); // Get all storage
console.log("All stored settings:", data);
return data;
} catch (error) {
console.error("Error retrieving settings:", error);
return null;
}
};
// Event listeners
document.addEventListener("DOMContentLoaded", async () => {
// First load settings to ensure we have the correct values
await loadSettings();
// Then load filter stats
await loadFilterlistStats();
// Explicitly update the update status to ensure it's not stuck
await updateNextUpdateStatus();
// Log current settings for debugging
console.log("Current update frequency:", updateFrequency.value);
await window.checkCurrentSettings();
});
saveButton.addEventListener("click", saveSettings);
if (themeSelect) {
themeSelect.addEventListener("change", (e) => {
applyTheme(e.target.value);
});
}
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (themeSelect && themeSelect.value === "system") {
applyTheme("system");
}
});
browserAPI.runtime.onMessage.addListener((message) => {
if (message.type === "filterlistUpdated") {
loadFilterlistStats();
}
});
setInterval(updateNextUpdateStatus, 60000);
});