Version 2 improvements with UI and functionality
This commit is contained in:
+428
-38
@@ -2,14 +2,15 @@
|
||||
let currentRules = [];
|
||||
let editingRuleIndex = -1;
|
||||
let ruleModal;
|
||||
let scannedMedia = [];
|
||||
let harvestedLinks = [];
|
||||
let scrapedData = [];
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Wait for Bootstrap to be fully loaded
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
initializeExtension();
|
||||
} else {
|
||||
// Fallback: wait a bit for Bootstrap to load
|
||||
setTimeout(initializeExtension, 100);
|
||||
}
|
||||
});
|
||||
@@ -20,21 +21,22 @@ function initializeExtension() {
|
||||
} catch (e) {
|
||||
console.error('Error initializing modal:', e);
|
||||
}
|
||||
|
||||
|
||||
loadRules();
|
||||
setupEventListeners();
|
||||
setupAdvancedRuleFields();
|
||||
setupHeaderControls();
|
||||
setupToolsTab();
|
||||
restorePendingPicker();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
const addRuleBtn = document.getElementById('addRuleBtn');
|
||||
const saveRuleBtn = document.getElementById('saveRuleBtn');
|
||||
const matchTypeSelect = document.getElementById('matchType');
|
||||
|
||||
|
||||
if (addRuleBtn) {
|
||||
addRuleBtn.addEventListener('click', () => {
|
||||
console.log('Add Rule button clicked');
|
||||
openRuleModal();
|
||||
});
|
||||
addRuleBtn.addEventListener('click', () => openRuleModal());
|
||||
} else {
|
||||
console.error('Add Rule button not found!');
|
||||
}
|
||||
@@ -96,7 +98,7 @@ function renderRules() {
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
|
||||
rulesList.innerHTML = currentRules.map((rule, index) => {
|
||||
const matchTypeLabel = {
|
||||
'url': 'URL',
|
||||
@@ -106,19 +108,24 @@ function renderRules() {
|
||||
|
||||
const hasJS = rule.javascript && rule.javascript.trim();
|
||||
const hasCSS = rule.css && rule.css.trim();
|
||||
const hasAutoClick = rule.autoClickSelector && rule.autoClickSelector.trim();
|
||||
const hasReplacements = rule.textReplacements && rule.textReplacements.length;
|
||||
|
||||
return `
|
||||
<div class="rule-card" data-rule-index="${index}">
|
||||
<div class="rule-header">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="rule-name">${escapeHtml(rule.name)}</span>
|
||||
<span class="badge rule-badge bg-secondary">${matchTypeLabel}</span>
|
||||
${hasJS ? '<span class="badge rule-badge bg-warning">JS</span>' : ''}
|
||||
${hasCSS ? '<span class="badge rule-badge bg-info">CSS</span>' : ''}
|
||||
${hasAutoClick ? '<span class="badge rule-badge bg-success">Click</span>' : ''}
|
||||
${hasReplacements ? '<span class="badge rule-badge bg-primary">Replace</span>' : ''}
|
||||
${rule.reapplyOnDomChanges ? '<span class="badge rule-badge bg-dark border">SPA</span>' : ''}
|
||||
</div>
|
||||
<div class="rule-actions">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input rule-toggle" type="checkbox" ${rule.enabled ? 'checked' : ''}
|
||||
<input class="form-check-input rule-toggle" type="checkbox" ${rule.enabled ? 'checked' : ''}
|
||||
data-index="${index}" title="Enable/Disable">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-light btn-edit-rule" data-index="${index}" title="Edit">
|
||||
@@ -141,19 +148,16 @@ function renderRules() {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add event listeners using event delegation
|
||||
|
||||
attachRuleEventListeners();
|
||||
}
|
||||
|
||||
function attachRuleEventListeners() {
|
||||
const rulesList = document.getElementById('rulesList');
|
||||
|
||||
// Remove old listeners if any
|
||||
|
||||
rulesList.removeEventListener('click', handleRuleClick);
|
||||
rulesList.removeEventListener('change', handleRuleToggle);
|
||||
|
||||
// Add new listeners
|
||||
|
||||
rulesList.addEventListener('click', handleRuleClick);
|
||||
rulesList.addEventListener('change', handleRuleToggle);
|
||||
}
|
||||
@@ -161,20 +165,17 @@ function attachRuleEventListeners() {
|
||||
function handleRuleClick(e) {
|
||||
const editBtn = e.target.closest('.btn-edit-rule');
|
||||
const deleteBtn = e.target.closest('.btn-delete-rule');
|
||||
|
||||
|
||||
if (editBtn) {
|
||||
const index = parseInt(editBtn.dataset.index);
|
||||
editRule(index);
|
||||
editRule(parseInt(editBtn.dataset.index));
|
||||
} else if (deleteBtn) {
|
||||
const index = parseInt(deleteBtn.dataset.index);
|
||||
deleteRule(index);
|
||||
deleteRule(parseInt(deleteBtn.dataset.index));
|
||||
}
|
||||
}
|
||||
|
||||
function handleRuleToggle(e) {
|
||||
if (e.target.classList.contains('rule-toggle')) {
|
||||
const index = parseInt(e.target.dataset.index);
|
||||
toggleRule(index);
|
||||
toggleRule(parseInt(e.target.dataset.index));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +185,8 @@ function openRuleModal(ruleIndex = -1) {
|
||||
const form = document.getElementById('ruleForm');
|
||||
|
||||
form.reset();
|
||||
renderReplacementRows([]);
|
||||
document.getElementById('advancedSection').open = false;
|
||||
|
||||
if (ruleIndex >= 0) {
|
||||
modalTitle.textContent = 'Edit Rule';
|
||||
@@ -194,19 +197,29 @@ function openRuleModal(ruleIndex = -1) {
|
||||
document.getElementById('elementSelector').value = rule.elementSelector || '';
|
||||
document.getElementById('javascript').value = rule.javascript || '';
|
||||
document.getElementById('css').value = rule.css || '';
|
||||
document.getElementById('autoClickSelector').value = rule.autoClickSelector || '';
|
||||
document.getElementById('autoClickInterval').value = rule.autoClickIntervalSec || 0;
|
||||
document.getElementById('autoClickMax').value = rule.autoClickMax || 0;
|
||||
document.getElementById('reapplyOnDomChanges').checked = !!rule.reapplyOnDomChanges;
|
||||
document.getElementById('ruleEnabled').checked = rule.enabled;
|
||||
renderReplacementRows(rule.textReplacements || []);
|
||||
|
||||
if (rule.autoClickSelector || (rule.textReplacements && rule.textReplacements.length) || rule.reapplyOnDomChanges) {
|
||||
document.getElementById('advancedSection').open = true;
|
||||
}
|
||||
} else {
|
||||
modalTitle.textContent = 'Add Rule';
|
||||
document.getElementById('ruleEnabled').checked = true;
|
||||
}
|
||||
|
||||
updateFieldVisibility(document.getElementById('matchType').value);
|
||||
|
||||
// Show modal - ensure Bootstrap Modal is initialized
|
||||
showRuleModal();
|
||||
}
|
||||
|
||||
function showRuleModal() {
|
||||
if (ruleModal) {
|
||||
ruleModal.show();
|
||||
} else {
|
||||
// Fallback: try to initialize and show
|
||||
try {
|
||||
ruleModal = new bootstrap.Modal(document.getElementById('ruleModal'));
|
||||
ruleModal.show();
|
||||
@@ -225,8 +238,12 @@ function saveRule() {
|
||||
const javascript = document.getElementById('javascript').value;
|
||||
const css = document.getElementById('css').value;
|
||||
const enabled = document.getElementById('ruleEnabled').checked;
|
||||
const autoClickSelector = document.getElementById('autoClickSelector').value.trim();
|
||||
const autoClickIntervalSec = parseInt(document.getElementById('autoClickInterval').value, 10) || 0;
|
||||
const autoClickMax = parseInt(document.getElementById('autoClickMax').value, 10) || 0;
|
||||
const reapplyOnDomChanges = document.getElementById('reapplyOnDomChanges').checked;
|
||||
const textReplacements = collectReplacementRows().filter((r) => r.find.trim());
|
||||
|
||||
// Validation
|
||||
if (!name) {
|
||||
alert('Please enter a rule name');
|
||||
return;
|
||||
@@ -247,19 +264,14 @@ function saveRule() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!javascript && !css) {
|
||||
alert('Please enter either JavaScript or CSS code');
|
||||
if (!javascript && !css && !autoClickSelector && textReplacements.length === 0) {
|
||||
alert('Please enter JavaScript, CSS, an auto-click selector, or a text replacement');
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = {
|
||||
name,
|
||||
matchType,
|
||||
urlPattern,
|
||||
elementSelector,
|
||||
javascript,
|
||||
css,
|
||||
enabled
|
||||
name, matchType, urlPattern, elementSelector, javascript, css, enabled,
|
||||
autoClickSelector, autoClickIntervalSec, autoClickMax, reapplyOnDomChanges, textReplacements
|
||||
};
|
||||
|
||||
if (editingRuleIndex >= 0) {
|
||||
@@ -296,7 +308,385 @@ function toggleRule(index) {
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
div.textContent = text == null ? '' : text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Advanced rule fields: text replacement rows + the "pick on page" button
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
function setupAdvancedRuleFields() {
|
||||
document.getElementById('addReplacementBtn').addEventListener('click', () => {
|
||||
const rows = collectReplacementRows();
|
||||
rows.push({ find: '', replace: '' });
|
||||
renderReplacementRows(rows);
|
||||
});
|
||||
|
||||
document.getElementById('textReplacementsList').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.remove-replacement-btn');
|
||||
if (!btn) return;
|
||||
const rows = collectReplacementRows();
|
||||
const index = parseInt(btn.closest('.replacement-row').dataset.index, 10);
|
||||
rows.splice(index, 1);
|
||||
renderReplacementRows(rows);
|
||||
});
|
||||
|
||||
document.getElementById('pickElementBtn').addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab || !tab.id) return;
|
||||
|
||||
await browser.storage.local.set({ pickerPending: collectModalSnapshot() });
|
||||
|
||||
try {
|
||||
await browser.tabs.sendMessage(tab.id, { action: 'startPicker' });
|
||||
} catch (e) {
|
||||
alert('Could not start the element picker on this page (it may be a restricted browser page).');
|
||||
return;
|
||||
}
|
||||
// The popup closes as soon as focus moves to the page, so there's nothing more
|
||||
// to do here — the picked selector is relayed back through storage and restored
|
||||
// the next time this popup opens (see restorePendingPicker()).
|
||||
});
|
||||
}
|
||||
|
||||
function renderReplacementRows(rows) {
|
||||
const list = document.getElementById('textReplacementsList');
|
||||
list.innerHTML = rows.map((r, i) => `
|
||||
<div class="input-group input-group-sm mb-1 replacement-row" data-index="${i}">
|
||||
<input type="text" class="form-control replacement-find" placeholder="Find" value="${escapeHtml(r.find || '')}">
|
||||
<input type="text" class="form-control replacement-replace" placeholder="Replace with" value="${escapeHtml(r.replace || '')}">
|
||||
<button class="btn btn-outline-danger remove-replacement-btn" type="button" title="Remove">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function collectReplacementRows() {
|
||||
return [...document.querySelectorAll('.replacement-row')].map((row) => ({
|
||||
find: row.querySelector('.replacement-find').value,
|
||||
replace: row.querySelector('.replacement-replace').value
|
||||
}));
|
||||
}
|
||||
|
||||
function collectModalSnapshot() {
|
||||
return {
|
||||
modalTitle: document.getElementById('modalTitle').textContent,
|
||||
editingRuleIndex,
|
||||
ruleName: document.getElementById('ruleName').value,
|
||||
matchType: document.getElementById('matchType').value,
|
||||
urlPattern: document.getElementById('urlPattern').value,
|
||||
elementSelector: document.getElementById('elementSelector').value,
|
||||
javascript: document.getElementById('javascript').value,
|
||||
css: document.getElementById('css').value,
|
||||
autoClickSelector: document.getElementById('autoClickSelector').value,
|
||||
autoClickInterval: document.getElementById('autoClickInterval').value,
|
||||
autoClickMax: document.getElementById('autoClickMax').value,
|
||||
reapplyOnDomChanges: document.getElementById('reapplyOnDomChanges').checked,
|
||||
textReplacements: collectReplacementRows(),
|
||||
ruleEnabled: document.getElementById('ruleEnabled').checked
|
||||
};
|
||||
}
|
||||
|
||||
function restoreModalState(snapshot, pickedSelector) {
|
||||
editingRuleIndex = snapshot.editingRuleIndex;
|
||||
document.getElementById('modalTitle').textContent = snapshot.modalTitle;
|
||||
document.getElementById('ruleName').value = snapshot.ruleName;
|
||||
document.getElementById('matchType').value = snapshot.matchType;
|
||||
document.getElementById('urlPattern').value = snapshot.urlPattern;
|
||||
document.getElementById('elementSelector').value = pickedSelector || snapshot.elementSelector;
|
||||
document.getElementById('javascript').value = snapshot.javascript;
|
||||
document.getElementById('css').value = snapshot.css;
|
||||
document.getElementById('autoClickSelector').value = snapshot.autoClickSelector;
|
||||
document.getElementById('autoClickInterval').value = snapshot.autoClickInterval;
|
||||
document.getElementById('autoClickMax').value = snapshot.autoClickMax;
|
||||
document.getElementById('reapplyOnDomChanges').checked = snapshot.reapplyOnDomChanges;
|
||||
document.getElementById('ruleEnabled').checked = snapshot.ruleEnabled;
|
||||
renderReplacementRows(snapshot.textReplacements || []);
|
||||
document.getElementById('advancedSection').open = true;
|
||||
updateFieldVisibility(snapshot.matchType);
|
||||
showRuleModal();
|
||||
}
|
||||
|
||||
function restorePendingPicker() {
|
||||
browser.storage.local.get(['pickerPending', 'pendingPickResult']).then((result) => {
|
||||
if (!result.pickerPending) return;
|
||||
restoreModalState(result.pickerPending, result.pendingPickResult || null);
|
||||
browser.storage.local.remove(['pickerPending', 'pendingPickResult']);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Header controls: master toggle, dark mode quick action, import/export
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
function setupHeaderControls() {
|
||||
const masterToggle = document.getElementById('masterToggle');
|
||||
const masterToggleLabel = document.getElementById('masterToggleLabel');
|
||||
|
||||
browser.storage.sync.get(['masterEnabled']).then((result) => {
|
||||
const enabled = result.masterEnabled !== false;
|
||||
masterToggle.checked = enabled;
|
||||
masterToggleLabel.textContent = enabled ? 'On' : 'Off';
|
||||
});
|
||||
|
||||
masterToggle.addEventListener('change', () => {
|
||||
browser.storage.sync.set({ masterEnabled: masterToggle.checked }).then(() => {
|
||||
masterToggleLabel.textContent = masterToggle.checked ? 'On' : 'Off';
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('darkModeBtn').addEventListener('click', addDarkModeRule);
|
||||
document.getElementById('exportRulesBtn').addEventListener('click', exportRules);
|
||||
document.getElementById('importRulesBtn').addEventListener('click', () => {
|
||||
document.getElementById('importFileInput').click();
|
||||
});
|
||||
document.getElementById('importFileInput').addEventListener('change', importRules);
|
||||
}
|
||||
|
||||
async function addDarkModeRule() {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab || !tab.url) return;
|
||||
|
||||
let hostname;
|
||||
try {
|
||||
hostname = new URL(tab.url).hostname;
|
||||
} catch (e) {
|
||||
alert('Cannot create a dark mode rule for this page.');
|
||||
return;
|
||||
}
|
||||
if (!hostname) {
|
||||
alert('Cannot create a dark mode rule for this page.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = {
|
||||
name: `Dark Mode: ${hostname}`,
|
||||
matchType: 'url',
|
||||
urlPattern: hostname,
|
||||
elementSelector: '',
|
||||
javascript: '',
|
||||
css: 'html { filter: invert(1) hue-rotate(180deg); }\n' +
|
||||
'img, video, picture, canvas, svg, [style*="background-image"] { filter: invert(1) hue-rotate(180deg); }',
|
||||
enabled: true,
|
||||
autoClickSelector: '',
|
||||
autoClickIntervalSec: 0,
|
||||
autoClickMax: 0,
|
||||
reapplyOnDomChanges: false,
|
||||
textReplacements: []
|
||||
};
|
||||
|
||||
currentRules.push(rule);
|
||||
browser.storage.sync.set({ rules: currentRules }).then(() => {
|
||||
loadRules();
|
||||
alert(`Added a dark mode rule for ${hostname}. Reload the page to see it — edit or disable it any time from the Rules tab.`);
|
||||
});
|
||||
}
|
||||
|
||||
function exportRules() {
|
||||
downloadBlob(JSON.stringify(currentRules, null, 2), 'greasyboii-rules.json', 'application/json');
|
||||
}
|
||||
|
||||
function importRules() {
|
||||
const input = document.getElementById('importFileInput');
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
let imported;
|
||||
try {
|
||||
imported = JSON.parse(reader.result);
|
||||
} catch (e) {
|
||||
alert('That file is not valid JSON.');
|
||||
return;
|
||||
}
|
||||
|
||||
const looksLikeRules = Array.isArray(imported) &&
|
||||
imported.every((r) => r && typeof r.name === 'string' && typeof r.matchType === 'string');
|
||||
if (!looksLikeRules) {
|
||||
alert('That file does not look like a GreasyBoii rules export.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Import ${imported.length} rule(s) from this file?`)) return;
|
||||
const replace = confirm('Replace all existing rules with the imported ones?\n\nOK = Replace\nCancel = Add to existing rules');
|
||||
currentRules = replace ? imported : currentRules.concat(imported);
|
||||
|
||||
browser.storage.sync.set({ rules: currentRules }).then(loadRules);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Tools tab: bulk media downloader, link harvester, page data scraper
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
function setupToolsTab() {
|
||||
setupMediaDownloader();
|
||||
setupLinkHarvester();
|
||||
setupDataScraper();
|
||||
}
|
||||
|
||||
function setupMediaDownloader() {
|
||||
const mediaType = document.getElementById('mediaType');
|
||||
const mediaExtensions = document.getElementById('mediaExtensions');
|
||||
const scanMediaBtn = document.getElementById('scanMediaBtn');
|
||||
const mediaResults = document.getElementById('mediaResults');
|
||||
const downloadMediaBtn = document.getElementById('downloadMediaBtn');
|
||||
|
||||
mediaType.addEventListener('change', () => {
|
||||
mediaExtensions.style.display = mediaType.value === 'custom' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
scanMediaBtn.addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab) return;
|
||||
|
||||
try {
|
||||
const response = await browser.tabs.sendMessage(tab.id, {
|
||||
action: 'scanMedia',
|
||||
mediaType: mediaType.value,
|
||||
extensions: mediaExtensions.value
|
||||
});
|
||||
scannedMedia = (response && response.items) || [];
|
||||
mediaResults.textContent = `${scannedMedia.length} file(s) found.`;
|
||||
downloadMediaBtn.disabled = scannedMedia.length === 0;
|
||||
downloadMediaBtn.textContent = `Download All (${scannedMedia.length})`;
|
||||
} catch (e) {
|
||||
mediaResults.textContent = 'Could not scan this page (try reloading it first).';
|
||||
}
|
||||
});
|
||||
|
||||
downloadMediaBtn.addEventListener('click', () => {
|
||||
scannedMedia.forEach((item, i) => {
|
||||
setTimeout(() => {
|
||||
browser.downloads.download({ url: item.url, filename: item.filename, conflictAction: 'uniquify' })
|
||||
.catch((e) => console.error('[GreasyBoii] Download failed for', item.url, e));
|
||||
}, i * 150);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupLinkHarvester() {
|
||||
const containerSelector = document.getElementById('linkContainerSelector');
|
||||
const harvestLinksBtn = document.getElementById('harvestLinksBtn');
|
||||
const linkResults = document.getElementById('linkResults');
|
||||
const openLinksBtn = document.getElementById('openLinksBtn');
|
||||
const exportLinksBtn = document.getElementById('exportLinksBtn');
|
||||
|
||||
harvestLinksBtn.addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab) return;
|
||||
|
||||
try {
|
||||
const response = await browser.tabs.sendMessage(tab.id, {
|
||||
action: 'harvestLinks',
|
||||
containerSelector: containerSelector.value.trim()
|
||||
});
|
||||
harvestedLinks = (response && response.items) || [];
|
||||
renderLinkResults();
|
||||
} catch (e) {
|
||||
linkResults.textContent = 'Could not read links on this page (try reloading it first).';
|
||||
openLinksBtn.disabled = true;
|
||||
exportLinksBtn.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
function renderLinkResults() {
|
||||
if (harvestedLinks.length === 0) {
|
||||
linkResults.textContent = 'No links found.';
|
||||
openLinksBtn.disabled = true;
|
||||
exportLinksBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
linkResults.innerHTML = `<div class="tool-scroll">${harvestedLinks.map((link, i) => `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input link-check" type="checkbox" id="link-${i}" data-index="${i}" checked>
|
||||
<label class="form-check-label text-truncate d-block" for="link-${i}" title="${escapeHtml(link.url)}">${escapeHtml(link.text || link.url)}</label>
|
||||
</div>
|
||||
`).join('')}</div><div class="text-muted mt-1">${harvestedLinks.length} link(s) found</div>`;
|
||||
openLinksBtn.disabled = false;
|
||||
exportLinksBtn.disabled = false;
|
||||
}
|
||||
|
||||
function getCheckedLinks() {
|
||||
const checkedIndexes = [...document.querySelectorAll('.link-check:checked')].map((el) => parseInt(el.dataset.index, 10));
|
||||
return harvestedLinks.filter((_, i) => checkedIndexes.includes(i));
|
||||
}
|
||||
|
||||
openLinksBtn.addEventListener('click', () => {
|
||||
getCheckedLinks().forEach((link) => browser.tabs.create({ url: link.url, active: false }));
|
||||
});
|
||||
|
||||
exportLinksBtn.addEventListener('click', () => {
|
||||
const text = getCheckedLinks().map((l) => l.url).join('\n');
|
||||
downloadBlob(text, 'greasyboii-links.txt', 'text/plain');
|
||||
});
|
||||
}
|
||||
|
||||
function setupDataScraper() {
|
||||
const selector = document.getElementById('scrapeSelector');
|
||||
const attribute = document.getElementById('scrapeAttribute');
|
||||
const customAttr = document.getElementById('scrapeCustomAttr');
|
||||
const scrapeBtn = document.getElementById('scrapeBtn');
|
||||
const scrapeResults = document.getElementById('scrapeResults');
|
||||
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
||||
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||
|
||||
attribute.addEventListener('change', () => {
|
||||
customAttr.style.display = attribute.value === 'custom' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
scrapeBtn.addEventListener('click', async () => {
|
||||
const tab = await getActiveTab();
|
||||
if (!tab || !selector.value.trim()) return;
|
||||
|
||||
const attr = attribute.value === 'custom' ? customAttr.value.trim() : attribute.value;
|
||||
|
||||
try {
|
||||
const response = await browser.tabs.sendMessage(tab.id, {
|
||||
action: 'scrapeData',
|
||||
selector: selector.value.trim(),
|
||||
attribute: attr
|
||||
});
|
||||
scrapedData = (response && response.items) || [];
|
||||
scrapeResults.textContent = `${scrapedData.length} value(s) scraped.`;
|
||||
exportCsvBtn.disabled = scrapedData.length === 0;
|
||||
exportJsonBtn.disabled = scrapedData.length === 0;
|
||||
} catch (e) {
|
||||
scrapeResults.textContent = 'Could not scrape this page (try reloading it first).';
|
||||
}
|
||||
});
|
||||
|
||||
exportCsvBtn.addEventListener('click', () => {
|
||||
const csv = scrapedData.map((v) => `"${String(v).replace(/"/g, '""')}"`).join('\n');
|
||||
downloadBlob(csv, 'greasyboii-scrape.csv', 'text/csv');
|
||||
});
|
||||
|
||||
exportJsonBtn.addEventListener('click', () => {
|
||||
downloadBlob(JSON.stringify(scrapedData, null, 2), 'greasyboii-scrape.json', 'application/json');
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function getActiveTab() {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
return tabs[0];
|
||||
}
|
||||
|
||||
function downloadBlob(content, filename, mimeType) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user