var DEFAULT_REDIRECTS = [ { "description": "youtube to invidious", "exampleUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "exampleResult": "https://yewtu.be/watch?v=dQw4w9WgXcQ", "error": null, "includePattern": "https://www.youtube.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://yewtu.be/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "twitter to nitter", "exampleUrl": "https://twitter.com/fsf/status/1376714994721689601", "exampleResult": "https://nitter.net/fsf/status/1376714994721689601", "error": null, "includePattern": "https://twitter.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://nitter.net/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "reddit to teddit", "exampleUrl": "https://www.reddit.com/r/webhosting/comments/izq39w/hiding_public_ip/", "exampleResult": "https://teddit.net/r/webhosting/comments/izq39w/hiding_public_ip/", "error": null, "includePattern": "https://www.reddit.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://teddit.net/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": true, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "old reddit to teddit", "exampleUrl": "https://old.reddit.com/r/webhosting/comments/izq39w/hiding_public_ip/", "exampleResult": "https://teddit.net/r/webhosting/comments/izq39w/hiding_public_ip/", "error": null, "includePattern": "https://old.reddit.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://teddit.net/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": true, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "Google to ddg", "exampleUrl": "https://www.google.com/search?q=example", "exampleResult": "https://html.duckduckgo.com/html?q=example", "error": null, "includePattern": "https://www.google.com/search?q=*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://html.duckduckgo.com/html?q=$1", "patternType": "W", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "instagram to bibliogram", "exampleUrl": "https://www.instagram.com/natgeo", "exampleResult": "https://bibliogram.froth.zone/u/natgeo", "error": null, "includePattern": "https://www.instagram.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://bibliogram.froth.zone/u/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "ddg to ddg html", "exampleUrl": "https://duckduckgo.com/?t=ffsb&q=xml", "exampleResult": "https://html.duckduckgo.com/html?q=xml", "error": null, "includePattern": "https://duckduckgo.com/(.*)&q=(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://html.duckduckgo.com/html?q=$2", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "imgur to raw image", "exampleUrl": "https://imgur.com/nWjVtF1", "exampleResult": "https://i.imgur.com/nWjVtF1", "error": null, "includePattern": "https://imgur.com/*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://i.imgur.com/$1", "patternType": "W", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "php manual to english", "exampleUrl": "https://www.php.net/manual/ru/function.debug-backtrace.php", "exampleResult": "https://www.php.net/manual/en/function.debug-backtrace.php", "error": null, "includePattern": "https://www.php.net/manual/ru/*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://www.php.net/manual/en/$1", "patternType": "W", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "google sheets", "exampleUrl": "https://docs.google.com/spreadsheets/d/1cKXeUVJ2W2y7FjwnqchNrJllnm11fBW0xWWTF97ewE8/edit?usp=drivesdk", "exampleResult": "https://docs.google.com/spreadsheets/d/1cKXeUVJ2W2y7FjwnqchNrJllnm11fBW0xWWTF97ewE8/export?format=ods", "error": null, "includePattern": "https://docs.google.com/spreadsheets/(.*)/.*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://docs.google.com/spreadsheets/$1/export?format=ods", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "google docs", "exampleUrl": "https://docs.google.com/document/d/1P4bAdnp8JqZRH9EXz4MrQzrb2oCwnJM-myy_jCXLMLo/edit", "exampleResult": "https://docs.google.com/document/d/1P4bAdnp8JqZRH9EXz4MrQzrb2oCwnJM-myy_jCXLMLo/export?format=odt", "error": null, "includePattern": "https://docs.google.com/document/(.*)/.*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://docs.google.com/document/$1/export?format=odt", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "ghc (hackage) -> ghc-9.2.2 (download.haskell.org)", "exampleUrl": "https://hackage.haskell.org/package/ghc/docs/GHC-Unit-State.html#v:lookupPackageName", "exampleResult": "https://downloads.haskell.org/ghc/9.2.2/docs/html/libraries/ghc-9.2.2/GHC-Unit-State.html#v:lookupPackageName", "error": null, "includePattern": "https://hackage.haskell.org/package/ghc/docs/*", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://downloads.haskell.org/ghc/9.2.2/docs/html/libraries/ghc-9.2.2/$1", "patternType": "W", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "remove ddg tracking", "exampleUrl": "https://duckduckgo.com/l/?uddg=https%3A%2F%2Fsimplecheatsheet.com%2Forg%2Dmode%2Dtables%2F&rut=dfcd39afaca8d93796ddc81b894abc0c6bb471b47cb709e7331a91a0f88a9ec4", "exampleResult": "https://simplecheatsheet.com/org-mode-tables/", "error": null, "includePattern": "https://duckduckgo.com/l/?uddg=*&rut=*", "excludePattern": "", "patternDesc": "", "redirectUrl": "$1", "patternType": "W", "processMatches": "urlDecode", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "tiktok to proxitok", "exampleUrl": "https://www.tiktok.com/@tiktok", "exampleResult": "https://proxitok.pabloferreiro.es/@tiktok", "error": null, "includePattern": "(.*//.*)(tiktok.com)(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://proxitok.pabloferreiro.es$3", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "Imgur -> rimgo", "exampleUrl": "https://imgur.com/a/H8M4rcp", "exampleResult": "https://rimgo.projectsegfau.lt/a/H8M4rcp", "error": null, "includePattern": "^https?://i?.?imgur.com(/.*)?$", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://rimgo.projectsegfau.lt$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "medium.com -> scribe.rip", "exampleUrl": "https://medium.com/@zw3rk/template-haskell-75c7b67f9718", "exampleResult": "https://scribe.rip/@zw3rk/template-haskell-75c7b67f9718", "error": null, "includePattern": "https://medium.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://scribe.rip/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "Quora to Quetre", "exampleUrl": "https://www.quora.com/Linux-Kernel-Why-are-we-using-two-variables-mm_users-and-mm_count-in-mm_struct?share=1", "exampleResult": "https://quetre.esmailelbob.xyz/Linux-Kernel-Why-are-we-using-two-variables-mm_users-and-mm_count-in-mm_struct?share=1", "error": null, "includePattern": "https:\\/\\/.{2,}\\.quora\\.com\\/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://quetre.esmailelbob.xyz/$1", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "IMDb to libremdb", "exampleUrl": "https://www.imdb.com/title/tt0258463/?ref_=tt_sims_tt_t_4", "exampleResult": "https://libremdb.nerdyfam.tech/title/tt0258463/?ref_=tt_sims_tt_t_4", "error": null, "includePattern": "https?:\\/\\/(www\\.)?imdb\\.com\\/(.*)", "excludePattern": "https?://(www\\.)?imdb\\.com/chart/toptv", "patternDesc": "", "redirectUrl": "https://libremdb.nerdyfam.tech/$2", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "fandom to breezewiki", "exampleUrl": "https://minecraft.fandom.com/wiki/Bricks", "exampleResult": "https://breezewiki.com/minecraft/wiki/Bricks", "error": null, "includePattern": "https://*.fandom.com/wiki/*", "excludePattern": "https://www.fandom.com/*", "patternDesc": "", "redirectUrl": "https://breezewiki.com/$1/wiki/$2", "patternType": "W", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "goodreads to biblioreads", "exampleUrl": "https://www.goodreads.com/book/show/5907.The_Hobbit", "exampleResult": "https://biblioreads.ml/book/show/5907.The_Hobbit", "error": null, "includePattern": "^(?:https?://)www\\.goodreads\\.com/(book|work|author|series|quotes)(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://biblioreads.ml/$1$2", "patternType": "R", "processMatches": "noProcessing", "disabled": true, "grouped": false, "appliesTo": [ "main_frame" ] }, { "description": "reddit to old reddit", "exampleUrl": "https://www.reddit.com/r/torrents/comments/g5yffq/qbittorrent_vs_transmission_how_are_you_guys_even/", "exampleResult": "https://old.reddit.com/r/torrents/comments/g5yffq/qbittorrent_vs_transmission_how_are_you_guys_even/", "error": null, "includePattern": "https://(www)?.reddit.com/(.*)", "excludePattern": "", "patternDesc": "", "redirectUrl": "https://old.reddit.com/$2", "patternType": "R", "processMatches": "noProcessing", "disabled": false, "grouped": false, "appliesTo": [ "main_frame" ] } ]; var REDIRECTS = []; // The global redirects list... var options = { isSyncEnabled : false }; var template; function normalize(r) { return new Redirect(r).toObject(); //Cleans out any extra props, and adds default values for missing ones. } // Saves the entire list of redirects to storage. function saveChanges() { // Clean them up so angular $$hash things and stuff don't get serialized. let arr = REDIRECTS.map(normalize); chrome.runtime.sendMessage({type:"save-redirects", redirects:arr}, function(response) { console.log(response.message); if(response.message.indexOf("Redirects failed to save") > -1){ showMessage(response.message, false); } else{ console.log('Saved ' + arr.length + ' redirects at ' + new Date() + '. Message from background page:' + response.message); } }); } function toggleSyncSetting() { chrome.runtime.sendMessage({type:"toggle-sync", isSyncEnabled: !options.isSyncEnabled}, function(response) { if(response.message === "sync-enabled"){ options.isSyncEnabled = true; showMessage('Sync is enabled!', true); } else if(response.message === "sync-disabled"){ options.isSyncEnabled = false; showMessage('Sync is disabled - local storage will be used!', true); } else if(response.message.indexOf("Sync Not Possible")>-1){ options.isSyncEnabled = false; chrome.storage.local.set({isSyncEnabled: $s.isSyncEnabled}, function(){ // console.log("set back to false"); }); showMessage(response.message, false); } else { alert(response.message) showMessage('Error occured when trying to change Sync settings. Look at the logs and raise an issue',false); } el('#storage-sync-option input').checked = options.isSyncEnabled; }); } function renderRedirects() { el('.redirect-rows').textContent = ''; for (let i=0; i < REDIRECTS.length; i++) { let r = REDIRECTS[i]; let node = template.cloneNode(true); node.removeAttribute('id'); renderSingleRedirect(node, r, i); el('.redirect-rows').appendChild(node); } } function renderSingleRedirect(node, redirect, index) { //Add extra props to help with rendering... if (index === 0) { redirect.$first = true; } if (index === REDIRECTS.length - 1) { redirect.$last = true; } redirect.$index = index; dataBind(node, redirect); node.setAttribute('data-index', index); for (let btn of node.querySelectorAll('.btn')) { btn.setAttribute('data-index', index); } let checkmark = node.querySelectorAll('.checkmark'); if(checkmark.length == 1) { checkmark[0].setAttribute('data-index', index); } //Remove extra props... delete redirect.$first; delete redirect.$last; delete redirect.$index; } function updateBindings() { let nodes = document.querySelectorAll('.redirect-row'); if (nodes.length !== REDIRECTS.length) { throw new Error('Mismatch in lengths, Redirects are ' + REDIRECTS.length + ', nodes are ' + nodes.length) } for (let i=0; i < nodes.length; i++) { let node = nodes[i]; let redirect = REDIRECTS[i]; renderSingleRedirect(node, redirect, i); } } function duplicateRedirect(index) { let redirect = new Redirect(REDIRECTS[index]); REDIRECTS.splice(index, 0, redirect); let newNode = template.cloneNode(true); newNode.removeAttribute('id'); el('.redirect-rows').appendChild(newNode); updateBindings(); saveChanges(); } function checkIfGroupingExists() { let grouping = REDIRECTS.map((row, i) => { return { row, index: i}}) .filter(result => result.row.grouped) .sort((a, b) => a.index - b.index); return grouping; } function toggleDisabled(index) { let grouping = checkIfGroupingExists(); if(grouping && grouping.length > 1) { for (let redirect of grouping) { let redirectDom = REDIRECTS[redirect.index]; redirectDom.disabled = !redirectDom.disabled redirectDom.grouped = !redirectDom.grouped let elm = document.querySelector("[data-index='" + (redirect.index).toString() + "']"); clearGrouping(elm); } } else { let redirect = REDIRECTS[index]; redirect.disabled = !redirect.disabled } updateBindings(); saveChanges(); } function clearGrouping(elm) { elm.classList.remove('grouped'); let checkMarkElm = elm.querySelector("label > .groupings"); let toggleBoxElm = elm.querySelector("input"); checkMarkElm.classList.remove("checkMarked"); toggleBoxElm.classList.remove("checked"); } function swap(node1, node2) { const afterNode2 = node2.nextElementSibling; const parent = node2.parentNode; node1.replaceWith(node2); parent.insertBefore(node1, afterNode2); } function groupedMoveDown(group) { var jumpLength = 1; if(isGroupAdjacent(group)) { jumpLength = group.length; } for(let rule of group) { let elm = document.querySelector("[data-index='" + (rule.index).toString() + "']"); let prev = document.querySelector("[data-index='" + (rule.index + jumpLength).toString() + "']"); clearGrouping(elm); clearGrouping(prev); swap(elm,prev); } for(let rule of group) { rule.row.grouped = false; let prevRedir = REDIRECTS[rule.index + jumpLength]; REDIRECTS[rule.index + jumpLength] = REDIRECTS[rule.index]; REDIRECTS[rule.index] = prevRedir; } updateBindings(); saveChanges(); } function isGroupAdjacent(grouping) { let distances = []; for(let i = grouping.length - 1; i >= 0; i--) { if(i != 0) { distances.push(grouping[i].index - grouping[i - 1].index); } } return distances.every(distance => distance === 1); } function groupedMoveUp(group) { var jumpLength = 1; if(isGroupAdjacent(group)) { jumpLength = group.length; } for(let rule of group) { let elm = document.querySelector("[data-index='" + (rule.index).toString() + "']"); let prev = document.querySelector("[data-index='" + (rule.index - jumpLength).toString() + "']"); clearGrouping(elm); clearGrouping(prev); if(jumpLength > 1) { swap(elm,prev); } } for(let rule of group) { rule.row.grouped = false; let prevRedir = REDIRECTS[rule.index - jumpLength]; REDIRECTS[rule.index - jumpLength] = REDIRECTS[rule.index]; REDIRECTS[rule.index] = prevRedir; } updateBindings(); saveChanges(); } function moveUp(index) { let grouping = checkIfGroupingExists(); if(grouping.length > 1) { groupedMoveUp(grouping); } else { let prev = REDIRECTS[index-1]; REDIRECTS[index-1] = REDIRECTS[index]; REDIRECTS[index] = prev; } updateBindings(); saveChanges(); } function moveDown(index) { let grouping = checkIfGroupingExists(); if(grouping.length > 1) { groupedMoveDown(grouping); } else { let next = REDIRECTS[index+1]; REDIRECTS[index+1] = REDIRECTS[index]; REDIRECTS[index] = next; } updateBindings(); saveChanges(); } function moveUpTop(index) { let top = REDIRECTS[0]; move(REDIRECTS, index, top); updateBindings(); saveChanges(); } function moveDownBottom(index) { let bottom = REDIRECTS.length - 1; move(REDIRECTS, index, bottom); updateBindings(); saveChanges(); } //All the setup stuff for the page function pageLoad() { template = el('#redirect-row-template'); template.parentNode.removeChild(template); //Need to proxy this through the background page, because Firefox gives us dead objects //nonsense when accessing chrome.storage directly. chrome.runtime.sendMessage({type: "get-redirects"}, function(response) { console.log('Received redirects message, count=' + response.redirects.length); for (var i=0; i < response.redirects.length; i++) { REDIRECTS.push(new Redirect(response.redirects[i])); } if (response.redirects.length === 0) { //Add example redirect for first time users... for (var i=0; i < DEFAULT_REDIRECTS.length; i++) { REDIRECTS.push(new Redirect(DEFAULT_REDIRECTS[i])); } saveChanges(); } renderRedirects(); }); chrome.storage.local.get({isSyncEnabled:false}, function(obj){ options.isSyncEnabled = obj.isSyncEnabled; el('#storage-sync-option').checked = options.isSyncEnabled; }); if(navigator.userAgent.toLowerCase().indexOf("chrome") > -1){ show('#storage-sync-option'); } //Setup event listeners el('#hide-message').addEventListener('click', hideMessage); el('#storage-sync-option input').addEventListener('click', toggleSyncSetting); el('.redirect-rows').addEventListener('click', function(ev) { if(ev.target.type == 'checkbox') { ev.target.nextElementSibling.classList.add("checkMarked"); ev.target.parentElement.parentElement.classList.add('grouped'); toggleGrouping(ev.target.index); } let action = ev.target.getAttribute('data-action'); //We clone and re-use nodes all the time, so instead of attaching and removing event handlers endlessly we just put //a data-action attribute on them with the name of the function that should be called... if (!action) { return; } let handler = window[action]; let index = parseInt(ev.target.getAttribute('data-index')); handler(index); }); } function updateFavicon(e) { let type = e.matches ? 'dark' : 'light' el('link[rel="shortcut icon"]').href = `images/icon-${type}-theme-32.png`; chrome.runtime.sendMessage({type: "update-icon"}); //Only works if this page is open, but still, better than nothing... } let mql = window.matchMedia('(prefers-color-scheme:dark)'); mql.onchange = updateFavicon; updateFavicon(mql); function toggleGrouping(index) { if(REDIRECTS[index]) { REDIRECTS[index].grouped = !REDIRECTS[index].grouped; } } pageLoad();