├── .gitignore ├── README.md ├── .editorconfig ├── icons ├── error.svg ├── icon.svg └── icon-light.svg ├── options.html ├── options.js ├── LICENSE ├── manifest.json ├── popup ├── urls-list.html ├── urls-list.css └── urls-list.js └── background.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | web-ext-artifacts 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URLs List 2 | 3 | A Firefox extension to list the URLs of all tabs from the current window as copyable plaintext. 4 | Also this extension can load a plaintext list of urls to individual tabs. 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ! 5 | 6 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UL 5 | 6 | -------------------------------------------------------------------------------- /icons/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UL 5 | 6 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | function saveOptions(e) { 2 | e.preventDefault(); 3 | browser.storage.sync.set({ 4 | showTabContextMenuCopyUrls: document.querySelector("#showTabContextMenuCopyUrls").checked, 5 | openUrlsAlreadyOpened: document.querySelector("#openUrlsAlreadyOpened").checked, 6 | }); 7 | browser.runtime.sendMessage({}); 8 | } 9 | 10 | function restoreOptions() { 11 | browser.storage.sync.get().then(settings => { 12 | let showContextMenu = ('showTabContextMenuCopyUrls' in settings) ? settings.showTabContextMenuCopyUrls : true; 13 | let openTabs = ('openUrlsAlreadyOpened' in settings) ? settings.openUrlsAlreadyOpened : false; 14 | document.querySelector("#showTabContextMenuCopyUrls").checked = showContextMenu; 15 | document.querySelector("#openUrlsAlreadyOpened").checked = openTabs; 16 | }, error => { 17 | console.log(`Error: ${error}`); 18 | }); 19 | } 20 | 21 | document.addEventListener("DOMContentLoaded", restoreOptions); 22 | document.querySelector("form").addEventListener("submit", saveOptions); 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Moritz Heinemann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "URLs List", 4 | "version": "0.6.0", 5 | "description": "List the URLs of all tabs from the current window as copyable plaintext.", 6 | "homepage_url": "https://github.com/moritz-h/urls-list", 7 | "browser_specific_settings": { 8 | "gecko": { 9 | "id": "{88664789-f91e-40e1-adb9-e4e9a8c48867}", 10 | "strict_min_version": "115.0" 11 | } 12 | }, 13 | "permissions": [ 14 | "activeTab", 15 | "clipboardWrite", 16 | "contextMenus", 17 | "notifications", 18 | "storage", 19 | "tabs" 20 | ], 21 | "background": { 22 | "scripts": [ 23 | "background.js" 24 | ] 25 | }, 26 | "browser_action": { 27 | "browser_style": true, 28 | "default_icon": "icons/icon.svg", 29 | "default_title": "URLs List", 30 | "default_popup": "popup/urls-list.html", 31 | "theme_icons": [{ 32 | "light": "icons/icon-light.svg", 33 | "dark": "icons/icon.svg", 34 | "size": 16 35 | }] 36 | }, 37 | "options_ui": { 38 | "page": "options.html", 39 | "browser_style": true 40 | }, 41 | "icons": { 42 | "48": "icons/icon.svg", 43 | "96": "icons/icon.svg" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /popup/urls-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
URLs List
12 |
13 |
14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 | Remove filter to edit textbox. 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /popup/urls-list.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 350px; 3 | } 4 | 5 | .panel { 6 | width: 100%; 7 | } 8 | 9 | .filterBox { 10 | padding-left: 6px; 11 | padding-top: 10px; 12 | } 13 | 14 | .sortButtons { 15 | float: right; 16 | padding-right: 6px; 17 | } 18 | 19 | .filterInput { 20 | width: 150px; 21 | } 22 | 23 | .filterInputError { 24 | background-color: #fbb; 25 | } 26 | 27 | .filterWarning { 28 | background-color: orange; 29 | text-align: center; 30 | padding-top: 3px; 31 | padding-bottom: 3px; 32 | } 33 | 34 | .urlText { 35 | height: 400px; 36 | margin-top: -1px; 37 | resize: none; 38 | white-space: pre; 39 | width: 100%; 40 | } 41 | 42 | .urlTextFilterMode { 43 | background-color: #ddd; 44 | } 45 | 46 | .buttonRow { 47 | padding: 5px; 48 | text-align: center; 49 | width: 100%; 50 | } 51 | 52 | .buttonRow > button { 53 | margin: 5px; 54 | } 55 | 56 | .hide { 57 | display: none; 58 | } 59 | 60 | @media (prefers-color-scheme: dark) { 61 | body { 62 | background-color: #42414D; 63 | color: #FFF; 64 | } 65 | 66 | input { 67 | background-color: #42414D; 68 | color: #FFF 69 | } 70 | 71 | textarea.browser-style { 72 | background-color: #42414D; 73 | color: #FFF 74 | } 75 | 76 | button.browser-style { 77 | background-color: #2B2A33; 78 | color: #FFF; 79 | } 80 | 81 | .urlTextFilterMode { 82 | background-color: #333 !important; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | function initContextMenu() { 2 | browser.storage.sync.get().then(settings => { 3 | let showEntry = ('showTabContextMenuCopyUrls' in settings) ? settings.showTabContextMenuCopyUrls : true; 4 | if (showEntry) { 5 | browser.contextMenus.create({ 6 | id: "url-list-copy-urls", 7 | title: "Copy URLs (all tabs)", 8 | contexts: ["tab"], 9 | }); 10 | browser.contextMenus.onClicked.addListener(onContextMenuClick); 11 | } 12 | }, error => { 13 | console.log(`Error: ${error}`); 14 | }); 15 | } 16 | 17 | function clearContextMenu() { 18 | browser.contextMenus.onClicked.removeListener(onContextMenuClick); 19 | browser.contextMenus.remove('url-list-copy-urls'); 20 | } 21 | 22 | function onContextMenuClick(info, tab) { 23 | if (info.menuItemId === "url-list-copy-urls") { 24 | browser.tabs.query({currentWindow: true}).then((tabs) => { 25 | let urls = tabs.map(tab => tab.url).join('\r\n'); 26 | 27 | navigator.clipboard.writeText(urls).then(() => { 28 | // success 29 | }, () => { 30 | notifyError(); 31 | }); 32 | }); 33 | } 34 | } 35 | 36 | function notifyError() { 37 | browser.notifications.create({ 38 | "type": "basic", 39 | "iconUrl": browser.runtime.getURL("icons/error.svg"), 40 | "title": "Error!", 41 | "message": "Writing to clipboard is not possible!" 42 | }); 43 | } 44 | 45 | function settingsChanged(message) { 46 | clearContextMenu(); 47 | initContextMenu(); 48 | } 49 | 50 | browser.runtime.onMessage.addListener(settingsChanged); 51 | initContextMenu(); 52 | -------------------------------------------------------------------------------- /popup/urls-list.js: -------------------------------------------------------------------------------- 1 | let resetBtn = document.querySelector('.reset'); 2 | let openBtn = document.querySelector('.open'); 3 | let copyBtn = document.querySelector('.copy'); 4 | let saveBtn = document.querySelector('.save'); 5 | let sortAscBtn = document.querySelector('.sortAsc'); 6 | let sortDescBtn = document.querySelector('.sortDesc'); 7 | let resetFilterBtn = document.querySelector('.resetFilter'); 8 | let urlText = document.querySelector('.urlText'); 9 | let filterInput = document.querySelector('.filterInput'); 10 | let filterWarning = document.querySelector('.filterWarning'); 11 | 12 | let alwaysOpenAllTabs = false; 13 | browser.storage.sync.get().then(settings => { 14 | alwaysOpenAllTabs = ('openUrlsAlreadyOpened' in settings) ? settings.openUrlsAlreadyOpened : false; 15 | }, error => { 16 | console.log(`Error: ${error}`); 17 | }); 18 | 19 | function listTabs() { 20 | disableFilterMode(); 21 | 22 | browser.tabs.query({currentWindow: true}).then((tabs) => { 23 | let urls = ''; 24 | for (let tab of tabs) { 25 | urls += tab.url + '\n'; 26 | } 27 | urlText.value = urls; 28 | }); 29 | } 30 | 31 | function open() { 32 | browser.tabs.query({currentWindow: true}).then((tabs) => { 33 | // save list of current urls 34 | let currentUrls = []; 35 | for (let tab of tabs) { 36 | currentUrls.push(tab.url); 37 | } 38 | let newUrls = urlText.value.split('\n'); 39 | for (let url of newUrls) { 40 | // only open if new url is not empty string and is not already opened 41 | if (url !== '' && (alwaysOpenAllTabs || currentUrls.indexOf(url) < 0)) { 42 | // prefix "http://" if it is not an url already 43 | if (url.indexOf('://') < 0) { 44 | url = 'http://' + url; 45 | } 46 | browser.tabs.create({ 47 | active: false, 48 | url: url 49 | }); 50 | } 51 | } 52 | }); 53 | } 54 | 55 | function copy() { 56 | let tmp = urlText.value; 57 | urlText.select(); 58 | document.execCommand('Copy'); 59 | 60 | // workaround to not have text selected after button click 61 | urlText.value = ''; 62 | urlText.value = tmp; 63 | } 64 | 65 | function save() { 66 | let d = new Date(); 67 | let year = d.getFullYear(); 68 | let month = ('0' + (d.getMonth() + 1)).slice(-2); 69 | let day = ('0' + d.getDate()).slice(-2); 70 | let hour = ('0' + d.getHours()).slice(-2); 71 | let min = ('0' + d.getMinutes()).slice(-2); 72 | let sec = ('0' + d.getSeconds()).slice(-2); 73 | let dateString = [year, month, day, hour, min, sec].join('-'); 74 | 75 | let dl = document.createElement('a'); 76 | 77 | dl.download = 'urls-list-' + dateString + '.txt'; // filename 78 | dl.href = window.URL.createObjectURL( 79 | new Blob([urlText.value], {type: 'text/plain'}) // file content 80 | ); 81 | dl.onclick = event => document.body.removeChild(event.target); 82 | dl.style.display = 'none'; 83 | document.body.appendChild(dl); 84 | dl.click(); 85 | } 86 | 87 | function sort(desc = false) { 88 | let urls = urlText.value.split('\n'); 89 | let cleanUrls = []; 90 | for (let i in urls) { 91 | let clean = urls[i].trim(); 92 | if (clean !== '') { 93 | cleanUrls.push(clean); 94 | } 95 | } 96 | cleanUrls.sort(); 97 | if (desc) { 98 | cleanUrls.reverse(); 99 | } 100 | urlText.value = cleanUrls.join('\n') + '\n'; 101 | } 102 | 103 | function sortAsc() { 104 | sort(false); 105 | } 106 | 107 | function sortDesc() { 108 | sort(true); 109 | } 110 | 111 | let filterBackup = ''; 112 | let filterMode = false; 113 | 114 | function enableFilterMode() { 115 | if (!filterMode) { 116 | filterBackup = urlText.value; 117 | urlText.readOnly = true; 118 | urlText.classList.add("urlTextFilterMode") 119 | filterWarning.classList.remove("hide"); 120 | filterMode = true; 121 | } 122 | } 123 | 124 | function disableFilterMode() { 125 | if (filterMode) { 126 | urlText.value = filterBackup; 127 | urlText.readOnly = false; 128 | urlText.classList.remove("urlTextFilterMode"); 129 | filterWarning.classList.add("hide"); 130 | filterInput.classList.remove("filterInputError"); 131 | filterInput.value = ''; 132 | filterMode = false; 133 | } 134 | } 135 | 136 | function filter(e) { 137 | let val = e.target.value; 138 | filterInput.classList.remove("filterInputError"); 139 | if (val !== '') { 140 | enableFilterMode(); 141 | try { 142 | let re = new RegExp(val, 'i'); 143 | let urls = filterBackup.split('\n'); 144 | let filteredUrls = []; 145 | for (let i in urls) { 146 | let clean = urls[i].trim(); 147 | if (clean !== '' && re.test(clean)) { 148 | filteredUrls.push(clean); 149 | } 150 | } 151 | urlText.value = filteredUrls.join('\n') + '\n'; 152 | } catch (ex) { 153 | filterInput.classList.add("filterInputError"); 154 | } 155 | } else { 156 | disableFilterMode(); 157 | } 158 | } 159 | 160 | function resetFilter() { 161 | disableFilterMode(); 162 | } 163 | 164 | document.addEventListener('DOMContentLoaded', listTabs); 165 | resetBtn.addEventListener('click', listTabs); 166 | openBtn.addEventListener('click', open); 167 | copyBtn.addEventListener('click', copy); 168 | saveBtn.addEventListener('click', save); 169 | sortAscBtn.addEventListener('click', sortAsc); 170 | sortDescBtn.addEventListener('click', sortDesc); 171 | resetFilterBtn.addEventListener('click', resetFilter); 172 | filterInput.addEventListener('input', filter); 173 | --------------------------------------------------------------------------------