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 + +
+
+
+ + + +
+
+

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 }); + } + } } } });