diff --git a/README.md b/README.md
index 98d6624..74d5830 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Large, corporate-run wiki farms have enabled hundreds of great wikis and communi
When visiting a wiki on a large corporate wiki farm such as Fandom, Indie Wiki Buddy will notify and/or automatically redirect you if a quality, independent alternative is available. You can customize your experience per-wiki.
-In addition, search results in Google, Bing, DuckDuckGo, Yahoo, Brave Search, Ecosia, Startpage, Qwant, Yandex, and Kagi can also be filtered, replacing non-independent wikis with text inviting you to visit the independent counterpart.
+In addition, search results in Google, Bing, DuckDuckGo, Yahoo, Brave Search, Ecosia, Startpage, Qwant, Yandex, Kagi, SearXNG, and Whoogle can also be filtered, replacing non-independent wikis with text inviting you to visit the independent counterpart.
Indie Wiki Buddy also supports [BreezeWiki](https://breezewiki.com/), a service that renders Fandom wikis without ads or bloat. This helps give you a more enjoyable reading experience on Fandom when an independent wiki isn't available.
diff --git a/pages/guide/index.html b/pages/guide/index.html
index ac36bf0..1859f03 100644
--- a/pages/guide/index.html
+++ b/pages/guide/index.html
@@ -101,8 +101,7 @@
When you visit a wiki on a large, corporate-run wiki host, this extension can notify or automatically redirect
you to quality independent wikis when they're available.
- Search results in Google, Bing, DuckDuckGo, Yahoo, Brave Search, Ecosia, Startpage, Qwant, Yandex, and Kagi can
- also be filtered,
+ Search results in Google, Bing, DuckDuckGo, Yahoo, Brave Search, Ecosia, Startpage, Qwant, Yandex, Kagi, SearXNG, and Whoogle can also be filtered,
replacing non-independent wikis with links to independent counterpart (or hiding them completely).
We currently redirect from Fandom, Fextralife, and Neoseeker wikis to independent counterparts.
diff --git a/pages/settings/index.html b/pages/settings/index.html
index e607085..94637c7 100644
--- a/pages/settings/index.html
+++ b/pages/settings/index.html
@@ -269,6 +269,40 @@
font-size: .9em;
}
+ #customSearchEnginesContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+ }
+
+ #customSearchEnginesList>div {
+ display: flex;
+ flex-direction: row;
+ padding: 0.5em .5em;
+ min-height: 20px;
+ align-items: center;
+ gap: 0.5em;
+ }
+
+ #customSearchEnginesList>div:hover {
+ background-color: #e8f0fe;
+ }
+
+ .customSearchEngineHostname {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ #customSearchEnginesAdd {
+ display: flex;
+ gap: 0.5em;
+ }
+
+ #customSearchEnginesAdd input {
+ width: 25em;
+ }
+
#legend div,
#legend span {
padding: 0 0 0 8px;
@@ -660,6 +694,22 @@
+
+
+ 🔍 Custom search engines
+
+
+
+
+
+
+ SearXNG
+ Whoogle
+
+ Add
+
+
+
Individual wiki settings
diff --git a/pages/settings/settings.js b/pages/settings/settings.js
index 206a484..4fbcad4 100644
--- a/pages/settings/settings.js
+++ b/pages/settings/settings.js
@@ -374,6 +374,43 @@ async function loadOptions(lang, textFilter = '') {
});
}
+function displayCustomSearchEngine(customSearchEngineHostname, customSearchEnginePreset) {
+ let customSearchEnginesList = document.getElementById('customSearchEnginesList');
+
+ let listItem = document.createElement('div');
+ listItem.classList.add('customSearchEngine');
+
+ let customSearchEngineHostnameLabel = document.createElement('span');
+ customSearchEngineHostnameLabel.classList.add('customSearchEngineHostname');
+ customSearchEngineHostnameLabel.innerText = customSearchEngineHostname;
+
+ let customSearchEnginePresetLabel = document.createElement('span');
+ customSearchEnginePresetLabel.classList.add('customSearchEnginePreset');
+ customSearchEnginePresetLabel.innerText = document.getElementById('newCustomSearchEnginePreset')
+ .querySelector(`option[value="${customSearchEnginePreset}"]`).innerText;
+
+ let customSearchEngineDeleteButton = document.createElement('button');
+ customSearchEngineDeleteButton.classList.add('customSearchEngineDelete');
+ customSearchEngineDeleteButton.innerText = 'Delete';
+ customSearchEngineDeleteButton.addEventListener('click', () => {
+ listItem.remove();
+
+ chrome.storage.sync.get({ 'customSearchEngines': {} }, (item) => {
+ let customSearchEngines = item.customSearchEngines;
+ delete customSearchEngines[customSearchEngineHostname];
+ chrome.storage.sync.set({ 'customSearchEngines': customSearchEngines });
+ });
+
+ chrome.scripting.unregisterContentScripts({ ids: [`content-search-filtering-${customSearchEngineHostname}`] });
+ });
+
+ listItem.appendChild(customSearchEngineHostnameLabel);
+ listItem.appendChild(customSearchEnginePresetLabel);
+ listItem.appendChild(customSearchEngineDeleteButton);
+
+ customSearchEnginesList.appendChild(listItem);
+}
+
// Set power setting
function setPower(setting, storeSetting = true) {
if (storeSetting) {
@@ -525,6 +562,66 @@ document.addEventListener('DOMContentLoaded', () => {
return false;
});
+ // Add event listener for adding custom search engine
+ function addCustomSearchEngine() {
+ let customSearchEngine = document.getElementById('newCustomSearchEngineDomain').value;
+
+ // Add "https://" if not already present
+ if (!customSearchEngine.includes('://')) {
+ customSearchEngine = 'https://' + customSearchEngine;
+ }
+ customSearchEngine = new URL(customSearchEngine);
+
+ // Check not already added
+ let hostnames = document.querySelectorAll('.customSearchEngineHostname');
+ for (let i = 0; i < hostnames.length; i++) {
+ if (hostnames[i].innerText === customSearchEngine.hostname) {
+ return;
+ }
+ }
+
+ chrome.permissions.request({
+ origins: [ `${customSearchEngine}*` ]
+ }, (granted) => {
+ // Callback is true if the user granted the permissions.
+ if (!granted) return;
+
+ chrome.scripting.registerContentScripts([{
+ id: `content-search-filtering-${customSearchEngine.hostname}`,
+ matches: [customSearchEngine + '*'],
+ js: [ '/scripts/common-functions.js', '/scripts/content-search-filtering.js' ],
+ runAt: "document_start"
+ }]);
+
+ let customSearchEnginePreset = document.getElementById('newCustomSearchEnginePreset').value;
+
+ chrome.storage.sync.get({ 'customSearchEngines': {} }, (item) => {
+ let customSearchEngines = item.customSearchEngines;
+ customSearchEngines[customSearchEngine.hostname] = customSearchEnginePreset;
+ chrome.storage.sync.set({ 'customSearchEngines': customSearchEngines });
+ });
+
+ displayCustomSearchEngine(customSearchEngine.hostname, customSearchEnginePreset);
+
+ document.getElementById('newCustomSearchEngineDomain').value = '';
+ });
+ }
+
+ document.getElementById('addCustomSearchEngine').addEventListener('click', () => {
+ addCustomSearchEngine();
+ });
+ document.getElementById('newCustomSearchEngineDomain').onkeyup = function(e) {
+ if (e.key === 'Enter') {
+ addCustomSearchEngine();
+ }
+ }
+
+ chrome.storage.sync.get({ 'customSearchEngines': {} }, (item) => {
+ Object.keys(item.customSearchEngines).forEach((key) => {
+ displayCustomSearchEngine(key, item.customSearchEngines[key]);
+ });
+ });
+
// Add event listeners for default action selections
document.querySelectorAll('[name="defaultWikiAction"]').forEach((el) => {
el.addEventListener('change', () => {
diff --git a/scripts/content-search-filtering.js b/scripts/content-search-filtering.js
index ef3163f..00204de 100644
--- a/scripts/content-search-filtering.js
+++ b/scripts/content-search-filtering.js
@@ -336,6 +336,12 @@ function hideSearchResults(searchResultContainer, searchEngine, site, showBanner
case 'kagi':
document.querySelector('#main').prepend(searchRemovalNotice);
break;
+ case 'searxng':
+ document.querySelector('#results').prepend(searchRemovalNotice);
+ break;
+ case 'whoogle':
+ document.querySelector('#main').prepend(searchRemovalNotice);
+ break;
default:
}
}
@@ -488,6 +494,12 @@ async function filterSearchResults(searchResults, searchEngine, storage) {
case 'kagi':
searchResultContainer = searchResult.closest('div.search-result, div.__srgi');
break;
+ case 'searxng':
+ searchResultContainer = searchResult.closest('article');
+ break;
+ case 'whoogle':
+ searchResultContainer = searchResult.closest('#main>div>div, details>div>div>div>div>div>div.has-favicon');
+ break;
default:
}
@@ -756,6 +768,46 @@ function main(mutations = null, observer = null) {
}
}, { once: true });
}
+ } else if (storage.customSearchEngines) {
+ function filterSearXNG() {
+ let searchResults = Array.from(document.querySelectorAll('h3>a')).filter(el =>
+ el.href?.includes('.fandom.com') ||
+ el.href?.includes('.wiki.fextralife.com') ||
+ el.href?.includes('.neoseeker.com/wiki/'));
+ filterSearchResults(searchResults, 'searxng', storage);
+ }
+
+ function filterWhoogle() {
+ let searchResults = Array.from(document.querySelectorAll('div>a')).filter(el =>
+ el.href?.includes('.fandom.com') ||
+ el.href?.includes('.wiki.fextralife.com') ||
+ el.href?.includes('.neoseeker.com/wiki/'));
+ filterSearchResults(searchResults, 'whoogle', storage);
+ }
+
+ function filter(searchEngine) {
+ if (searchEngine === 'searxng') {
+ filterSearXNG();
+ } else if (searchEngine === 'whoogle') {
+ filterWhoogle();
+ }
+ }
+
+ let customSearchEngines = storage.customSearchEngines;
+ if (customSearchEngines[currentURL.hostname]) {
+ let customSearchEnginePreset = customSearchEngines[currentURL.hostname];
+
+ // Wait for document to be interactive/complete:
+ if (['interactive', 'complete'].includes(document.readyState)) {
+ filter(customSearchEnginePreset);
+ } else {
+ document.addEventListener('readystatechange', e => {
+ if (['interactive', 'complete'].includes(document.readyState)) {
+ filter(customSearchEnginePreset);
+ }
+ }, { once: true });
+ }
+ }
}
}
});