diff --git a/README.md b/README.md index 4d82e8b..388feba 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ For any issues, bugs or concerns; please [Open an Issue](https://github.com/cssn * Extract All Links and Domains from Any Site * Extract Links from Selected Text on any Site * Extract Links from Clipboard or Any Text +* Extract Links from All Selected Tabs * Open Multiple Links in Tabs from Text * Download Links and Domains as a Text File * Copy the Text from a Link via Context Menu diff --git a/manifest-chrome.json b/manifest-chrome.json index b3842e7..f50481c 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -1,4 +1,5 @@ { + "optional_host_permissions": ["*://*/*"], "background": { "service_worker": "js/service-worker.js" }, diff --git a/manifest-firefox.json b/manifest-firefox.json index 254f31c..e43d930 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -1,4 +1,5 @@ { + "optional_permissions": ["*://*/*"], "background": { "scripts": ["js/service-worker.js"] }, diff --git a/manifest.json b/manifest.json index f78ca91..cae0100 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "description": "Easily extract, parse, or open all links/domains from a site or text with optional filters.", "homepage_url": "https://link-extractor.cssnr.com/", "author": "Shane", - "version": "0.4.4", + "version": "0.5.0", "manifest_version": 3, "commands": { "_execute_action": { diff --git a/src/html/options.html b/src/html/options.html index 219ddb3..7548d3f 100644 --- a/src/html/options.html +++ b/src/html/options.html @@ -110,6 +110,17 @@

Link Extractor

+
+ + Grant Host Permissions +
+
+ + Remove Host Permissions +
+
diff --git a/src/html/popup.html b/src/html/popup.html index ae79293..52b3ee4 100644 --- a/src/html/popup.html +++ b/src/html/popup.html @@ -14,6 +14,7 @@
+
All Links @@ -66,6 +67,12 @@
+ + Options
diff --git a/src/js/exports.js b/src/js/exports.js index c44f1f1..72e9fb7 100644 --- a/src/js/exports.js +++ b/src/js/exports.js @@ -3,9 +3,10 @@ /** * Inject extract.js to Tab and Open links.html with params * @function processLinks - * @param {String} filter Regex Filter - * @param {Boolean} domains Only Domains - * @param {Boolean} selection Only Selection + * @param {String} [filter] Regex Filter + * @param {Boolean} [domains] Only Domains + * @param {Boolean} [selection] Only Selection + * @param {chrome.tabs[]} tabs Tabs for Extraction */ export async function injectTab({ filter = null, @@ -13,16 +14,30 @@ export async function injectTab({ selection = false, } = {}) { console.log('injectTab:', filter, domains, selection) + const tabIds = [] - // Get Current Tab - const [tab] = await chrome.tabs.query({ currentWindow: true, active: true }) - console.debug(`tab: ${tab.id}`, tab) + // Extract tabIds from tabs + const tabs = await chrome.tabs.query({ highlighted: true }) + if (!tabs.length) { + const [tab] = await chrome.tabs.query({ + currentWindow: true, + active: true, + }) + console.debug(`tab: ${tab.id}`, tab) + tabIds.push(tab.id) + } else { + for (const tab of tabs) { + console.debug(`tab: ${tab.id}`, tab) + tabIds.push(tab.id) + } + } + console.log('tabIds:', tabIds) // Create URL to links.html const url = new URL(chrome.runtime.getURL('../html/links.html')) // Set URL searchParams - url.searchParams.set('tab', tab.id.toString()) + url.searchParams.set('tabs', tabIds.join(',')) if (filter) { url.searchParams.set('filter', filter) } @@ -34,11 +49,13 @@ export async function injectTab({ } // Inject extract.js which listens for messages - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ['/js/extract.js'], - }) - + for (const tab of tabIds) { + console.debug(`injecting tab.id: ${tab}`) + await chrome.scripting.executeScript({ + target: { tabId: tab }, + files: ['/js/extract.js'], + }) + } // Open Tab to links.html with desired params console.debug(`url: ${url.toString()}`) await chrome.tabs.create({ active: true, url: url.toString() }) @@ -113,10 +130,9 @@ export async function exportClick(event) { event.preventDefault() const name = event.target.dataset.importName console.debug('name:', name) - const display = event.target.dataset.importDisplay - console.debug('display:', display) + const display = event.target.dataset.importDisplay || name const data = await chrome.storage.sync.get() - console.debug('data:', data[name]) + // console.debug('data:', data[name]) if (!data[name].length) { return showToast(`No ${display} Found!`, 'warning') } @@ -146,8 +162,8 @@ export async function importChange(event) { event.preventDefault() const name = event.target.dataset.importName console.debug('name:', name) - const display = event.target.dataset.importDisplay - console.debug('display:', display) + const display = event.target.dataset.importDisplay || name + // console.debug('display:', display) const importInput = document.getElementById('import-input') if (!importInput.files?.length) { return console.debug('No importInput.files', importInput) @@ -198,3 +214,80 @@ export function textFileDownload(filename, text) { element.click() document.body.removeChild(element) } + +/** + * Request Host Permissions + * @function requestPerms + * @return {chrome.permissions.request} + */ +export async function requestPerms() { + return await chrome.permissions.request({ + origins: ['*://*/*'], + }) +} + +/** + * Check Host Permissions + * @function checkPerms + * @return {Boolean} + */ +export async function checkPerms() { + const hasPerms = await chrome.permissions.contains({ + origins: ['*://*/*'], + }) + console.debug('checkPerms:', hasPerms) + // Firefox still uses DOM Based Background Scripts + if (typeof document === 'undefined') { + return hasPerms + } + const hasPermsEl = document.querySelectorAll('.has-perms') + const grantPermsEl = document.querySelectorAll('.grant-perms') + if (hasPerms) { + hasPermsEl.forEach((el) => el.classList.remove('d-none')) + grantPermsEl.forEach((el) => el.classList.add('d-none')) + } else { + grantPermsEl.forEach((el) => el.classList.remove('d-none')) + hasPermsEl.forEach((el) => el.classList.add('d-none')) + } + return hasPerms +} + +/** + * Revoke Permissions Click Callback + * NOTE: For many reasons Chrome will determine host_perms are required and + * will ask for them at install time and not allow them to be revoked + * @function revokePerms + * @param {Event} event + */ +export async function revokePerms(event) { + console.debug('revokePerms:', event) + const permissions = await chrome.permissions.getAll() + console.debug('permissions:', permissions) + try { + await chrome.permissions.remove({ + origins: permissions.origins, + }) + await checkPerms() + } catch (e) { + console.log(e) + showToast(e.toString(), 'danger') + } +} + +/** + * Permissions On Added Callback + * @param permissions + */ +export async function onAdded(permissions) { + console.debug('onAdded', permissions) + await checkPerms() +} + +/** + * Permissions On Added Callback + * @param permissions + */ +export async function onRemoved(permissions) { + console.debug('onRemoved', permissions) + await checkPerms() +} diff --git a/src/js/links.js b/src/js/links.js index 4874930..990c754 100644 --- a/src/js/links.js +++ b/src/js/links.js @@ -39,18 +39,34 @@ const dtOptions = { async function initLinks() { console.log('initLinks: urlParams:', urlParams) try { - const tabId = parseInt(urlParams.get('tab')) + const tabIds = urlParams.get('tabs') + const tabs = tabIds.split(',') + console.log('tabs:', tabs) const selection = urlParams.has('selection') - console.debug(`tabId: ${tabId}, selection: ${selection}`) + // console.debug(`tabId: ${tabId}, selection: ${selection}`) - if (tabId) { - const action = selection ? 'selection' : 'all' - const links = await chrome.tabs.sendMessage(tabId, action) - await processLinks(links) + // TODO: Populate Links to links then processLinks + const allLinks = [] + if (tabs.length) { + console.log('processing tabs:', tabs) + // const tabId = parseInt(tabs[0]) + for (const tabId of tabs) { + console.log('tabId:', tabId) + const action = selection ? 'selection' : 'all' + console.log('action:', action) + const links = await chrome.tabs.sendMessage( + parseInt(tabId), + action + ) + allLinks.push(...links) + // await processLinks(links) + } } else { const { links } = await chrome.storage.local.get(['links']) - await processLinks(links) + allLinks.push(...links) + // await processLinks(links) } + await processLinks(allLinks) } catch (e) { console.warn('error:', e) alert('Error Processing Results. See Console for More Details...') diff --git a/src/js/main.js b/src/js/main.js index 05a3885..42d09e8 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -69,7 +69,7 @@ function showToast(message, type = 'success') { * @param {Function} fn * @param {Number} timeout */ -function debounce(fn, timeout = 300) { +function debounce(fn, timeout = 250) { let timeoutID return (...args) => { clearTimeout(timeoutID) diff --git a/src/js/options.js b/src/js/options.js index 5e82576..c67f58a 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -1,29 +1,39 @@ // JS for options.html import { + checkPerms, exportClick, importChange, importClick, + onAdded, + onRemoved, + requestPerms, + revokePerms, saveOptions, updateOptions, } from './exports.js' chrome.storage.onChanged.addListener(onChanged) +chrome.permissions.onAdded.addListener(onAdded) +chrome.permissions.onRemoved.addListener(onRemoved) + document.addEventListener('DOMContentLoaded', initOptions) document.addEventListener('blur', filterClick) document.addEventListener('click', filterClick) document.getElementById('update-filter').addEventListener('submit', filterClick) document.getElementById('filters-form').addEventListener('submit', addFilter) document.getElementById('reset-default').addEventListener('click', resetForm) -document - .querySelectorAll('[data-bs-toggle="tooltip"]') - .forEach((el) => new bootstrap.Tooltip(el)) +document.getElementById('grant-perms').addEventListener('click', grantPerms) +document.getElementById('revoke-perms').addEventListener('click', revokePerms) const optionsForm = document.getElementById('options-form') optionsForm.addEventListener('submit', (e) => e.preventDefault()) optionsForm .querySelectorAll('input, select') .forEach((el) => el.addEventListener('change', saveOptions)) +document + .querySelectorAll('[data-bs-toggle="tooltip"]') + .forEach((el) => new bootstrap.Tooltip(el)) // Data Import/Export document.getElementById('export-data').addEventListener('click', exportClick) @@ -53,6 +63,7 @@ async function initOptions() { document.getElementById('extractKey').textContent = commands.find((x) => x.name === 'extract').shortcut || 'Not Set' + await checkPerms() document.getElementById('add-filter').focus() } @@ -306,3 +317,13 @@ async function resetForm(event) { input.focus() await saveOptions(event) } + +/** + * Grant Permissions Click Callback + * @function grantPerms + * @param {MouseEvent} event + */ +export async function grantPerms(event) { + console.debug('grantPerms:', event) + await requestPerms() +} diff --git a/src/js/popup.js b/src/js/popup.js index c75705c..8c0f30c 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -1,11 +1,18 @@ // JS for popup.html -import { injectTab, saveOptions, updateOptions } from './exports.js' +import { + checkPerms, + injectTab, + requestPerms, + saveOptions, + updateOptions, +} from './exports.js' document.addEventListener('DOMContentLoaded', initPopup) document.getElementById('filter-form').addEventListener('submit', filterForm) document.getElementById('links-form').addEventListener('submit', linksForm) document.getElementById('links-text').addEventListener('input', updateLinks) +document.getElementById('grant-perms').addEventListener('click', grantPerms) document .querySelectorAll('a[href]') .forEach((el) => el.addEventListener('click', popupLinks)) @@ -19,6 +26,16 @@ document .querySelectorAll('[data-bs-toggle="tooltip"]') .forEach((el) => new bootstrap.Tooltip(el)) +// document.getElementById('allTabs').addEventListener('click', allTabs) +// +// async function allTabs(event) { +// console.debug('allTabs:', event) +// event.preventDefault() +// const tabs = await chrome.tabs.query({ highlighted: true }) +// console.debug('tabs:', tabs) +// await injectTab({ tabs: tabs }) +// } + /** * Initialize Popup * @function initOptions @@ -45,6 +62,14 @@ async function initPopup() { document.getElementById('homepage_url').href = manifest.homepage_url document.getElementById('filter-input').focus() + + const tabs = await chrome.tabs.query({ highlighted: true }) + console.debug('tabs:', tabs) + if (tabs.length > 1) { + console.info('Multiple Tabs Selected') + } + + await checkPerms() } /** @@ -206,3 +231,15 @@ function extractURLs(text) { } return urls } + +/** + * Grant Permissions Click Callback + * Promise from requestPerms is ignored so we can close the popup immediately + * @function grantPerms + * @param {MouseEvent} event + */ +export async function grantPerms(event) { + console.debug('grantPerms:', event) + requestPerms() + window.close() +}