├── .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 |
6 |
--------------------------------------------------------------------------------
/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/icons/icon-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
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 |
--------------------------------------------------------------------------------