// ==UserScript==
// @name Google SERP Scraper
// @namespace https://greasyfork.org/en/users/1467948-stonedkhajiit
// @version 0.1.0
// @description Scrape Google SERP results. View, filter, and export (JSON, CSV, MD, URLs). Configurable.
// @author StonedKhajiit
// @match https://www.google.com/*search?*
// @match https://www.google.*/*search?*
// @exclude /^https:\/\/www\.google\.[^/]+\/search\?.*(?:tbm=(?:isch|nws|shop|vid|bks|fin|app)|udm=(?:2|7|28|36)).*$/
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Application Namespace ---
const GSRS_App = {
// Core State
allResults: [],
filteredResults: [],
observer: null, // Will remain null
currentSettings: {},
// Script-wide state variables
state: {
filterTimeout: null,
copyDownloadTarget: 'current',
currentViewMode: 'list',
selectedResultListItem: null,
isMaximized: false,
originalPanelState: { top: '', left: '', width: '', height: '', right: '' },
originalTitleBarText: 'Google SERP Scraper',
selectorTestHighlightTimeout: null,
currentContextMenuItemData: null,
contextMenuHighlightTimeout: null,
lastListItemHighlightedElement: null,
},
// UI Element References
uiElements: {
uiContainer: null, resultsTextArea: null, uiMessageDiv: null, filterInput: null,
resultsCountSpan: null, observerStatusSpan: null, resultsListContainer: null,
resultPreviewArea: null, settingsPanel: null, contextMenuElement: null,
settingsDOMElements: {},
},
DEFAULT_SETTINGS: {
titleSelector: 'a h3',
// observerTargetSelector is no longer actively used for observing, but kept for settings UI
observerTargetSelector: '#rso, #search, #main, body',
uiContentVisible: true, uiSettingsVisible: false, uiPanelTop: '20px', uiPanelLeft: 'auto', uiPanelRight: '20px', uiPanelWidth: '380px',
debugMode: false, highlightParsed: true, highlightListItemOnPage: true,
showPreviewInListMode: true, showFilterInputArea: true, showDownloadActionsArea: true,
darkMode: false, lastFilterTerm: '',
fetchTitle: true, fetchUrl: true, fetchSiteName: true, fetchBreadcrumbs: true, fetchDescription: true, fetchDescriptionKeywords: true, fetchDateInfo: true,
decodeUrlsToReadable: true, hideDisabledFetchFields: true,
exportCsvMdPosition: true, exportCsvMdTitle: true, exportCsvMdUrl: true, exportCsvMdSiteName: true, exportCsvMdBreadcrumbs: true, exportCsvMdDescription: true, exportCsvMdHighlightedSnippets: true, exportCsvMdOriginalDateText: true, exportCsvMdParsedDateISO: true,
},
INTERNAL_SELECTORS: {
potentialResultBlockSelectors: ['div.xfX4Ac', 'div.MjjYud', 'div.g', 'div.tF2Cxc', 'div.Gx5Zad'],
ancestorToExcludeBlockIfInside: '', parentContainerToExcludeBlockIfInside: '',
knowledgePanelSelector: [ 'div.kp-wholepage', 'div[data-kpsecret]', 'div.kp-wholepage-osrp', 'div.bzXtMb.M8OgIe.dRpWwb', 'div.TQc1id.IVvPP' ].join(','),
knowledgePanelCoreContentDirectChild: [ ':scope > div[jscontroller="sG005c"]', ':scope > div.mod', ':scope > div.kp-UID', ':scope > div[data-hveid="CAkQCQ"]', ':scope > div.yTFeqb.wp-ms.oJxARb', ':scope > div.xpdopen', ':scope > div.SALvLe.k29K0b' ].join(','),
carouselStructureIndicator: 'div.XNfAUb, div.pla-carousel',
relatedQuestionsBlockHeadingSpan: 'span.mgAbYb.OSrXXb.RES9jf.IFnjPb',
relatedQuestionsBlockTextIndicators: ['相關問題', 'People also ask', 'Autres questions également posées', 'Ähnliche Fragen', 'Otras personas también preguntan', '関連する質問'],
individualRelatedQuestionPair: '.related-question-pair, div[jscontroller="xfmZMb"]',
outerDescriptionBlockSelector: 'div.kb0PBd[data-sncf^="1"]',
directDescriptionContainer: 'div.VwiC3b.yXK7lf:not(:has(div.fzUZNc)):not(.yfStGF)',
genericDescriptionContainer: 'div.VwiC3b:not([class*=" "]):not(:has(img)):not(:has(video)):not(:has(div.fzUZNc)):not(.yfStGF), div.VwiC3b.p4wth:not(.yXK7lf)',
videoDescriptionSelector: 'div.fzUZNc > div.ITZIwc.p4wth',
siteNameSelector: 'span.VuuXrf, .byrV5b .cHaqb:first-of-type',
citeDisplay: 'cite, .qLRx3b',
anchorInTitle: 'a',
dataAttributesForCandidate: ['data-hveid', 'data-ved'],
elementsToExcludeFromText: 'a, h1, h2, h3, h4, h5, h6, cite, button, form, input, script, style, nav, footer, .TbwUpd, .B6fmyf',
datePrefixSpanSelector: 'span.YrbPuc, span[style*="color:#70757a"]',
descriptionKeywordSelector: 'em.t55VCb, .VwiC3b span > em',
},
// Debounce timers (less critical now but kept for filter)
observerDebounceTimer: null,
URL_CHANGE_DEBOUNCE_DELAY: 500, // No longer used for auto-scraping
OBSERVER_DEBOUNCE_DELAY: 400, // No longer used for auto-scraping
};
// --- URL Change Detection Logic (Simplified as it's not the primary auto-trigger now) ---
GSRS_App.urlChangeDetector = {
lastUrl: '',
lastStartParam: undefined,
// This function will now mainly serve to update lastUrl and lastStartParam.
// Actual scraping on URL change is removed as per decision to abandon InfyScroll auto-load.
checkUrlChange: function(source = "manual_or_init") {
const currentUrl = window.location.href;
const currentQueryString = window.location.search;
const urlParams = new URLSearchParams(currentQueryString);
const newStartParam = urlParams.get('start');
if (GSRS_App.currentSettings.debugMode) {
console.log(`GSRS_Debug (URLCheck - Info Only): Source: ${source}, Current URL: ${currentUrl}, Last URL: ${this.lastUrl}, NewStart: ${newStartParam}, LastStart: ${this.lastStartParam}`);
}
if (currentUrl !== this.lastUrl) {
if (GSRS_App.currentSettings.debugMode) {
console.log(`GSRS (URLChange - Info Only): URL recorded as changed from "${this.lastUrl}" to "${currentUrl}"`);
}
}
this.lastUrl = currentUrl;
this.lastStartParam = newStartParam;
},
init: function() {
this.lastUrl = window.location.href; // Initialize with current URL
const initialParams = new URLSearchParams(window.location.search);
this.lastStartParam = initialParams.get('start'); // Initialize with current start param
if (GSRS_App.currentSettings.debugMode) {
console.log(`GSRS (URLChangeDetector Init - Info Only): Initial URL: ${this.lastUrl}, Initial 'start' param: ${this.lastStartParam}`);
}
// Override history methods to keep track of URL for informational purposes if needed,
// but they won't trigger scrapes.
const originalPushState = history.pushState;
history.pushState = function() {
const prev = window.location.href;
const result = originalPushState.apply(this, arguments);
if(window.location.href !== prev && GSRS_App.urlChangeDetector) GSRS_App.urlChangeDetector.checkUrlChange('history_pushstate');
return result;
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
const prev = window.location.href;
const result = originalReplaceState.apply(this, arguments);
if(window.location.href !== prev && GSRS_App.urlChangeDetector) GSRS_App.urlChangeDetector.checkUrlChange('history_replacestate');
return result;
};
window.addEventListener('popstate', () => {
if(GSRS_App.urlChangeDetector) GSRS_App.urlChangeDetector.checkUrlChange('popstate');
});
if (GSRS_App.currentSettings.debugMode) {
console.log("GSRS (URLChangeDetector - Info Only): Initialized. History API calls will be logged if debug mode is on.");
}
// Initial check to set the baseline URL and start param.
this.checkUrlChange('initial_setup');
}
};
// --- Settings Manager ---
GSRS_App.settingsManager = {
load: function() {
GSRS_App.currentSettings = { ...GSRS_App.DEFAULT_SETTINGS };
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
GSRS_App.currentSettings[key] = GM_getValue(`gsrs_${key}`, GSRS_App.DEFAULT_SETTINGS[key]);
});
GSRS_App.state.currentViewMode = GM_getValue('gsrs_lastViewMode', GSRS_App.state.currentViewMode);
if (GSRS_App.currentSettings.debugMode) console.log("GSRS: Settings & UI State loaded:", JSON.parse(JSON.stringify(GSRS_App.currentSettings)));
},
saveUIPrefs: function() {
GM_setValue('gsrs_uiContentVisible', GSRS_App.currentSettings.uiContentVisible);
GM_setValue('gsrs_uiSettingsVisible', GSRS_App.currentSettings.uiSettingsVisible);
const uiContainer = GSRS_App.uiElements.uiContainer;
if (uiContainer && !GSRS_App.state.isMaximized) {
const currentTop = uiContainer.style.top;
const currentLeft = uiContainer.style.left;
const currentRight = uiContainer.style.right;
GSRS_App.currentSettings.uiPanelTop = (currentTop && currentTop.endsWith('px')) ? currentTop : GSRS_App.DEFAULT_SETTINGS.uiPanelTop;
GM_setValue('gsrs_uiPanelTop', GSRS_App.currentSettings.uiPanelTop);
if (currentLeft && currentLeft !== 'auto') {
GSRS_App.currentSettings.uiPanelLeft = (currentLeft.endsWith('px')) ? currentLeft : GSRS_App.DEFAULT_SETTINGS.uiPanelLeft;
GSRS_App.currentSettings.uiPanelRight = 'auto';
GM_setValue('gsrs_uiPanelLeft', GSRS_App.currentSettings.uiPanelLeft);
GM_setValue('gsrs_uiPanelRight', 'auto');
} else if (currentRight && currentRight !== 'auto') {
GSRS_App.currentSettings.uiPanelRight = (currentRight.endsWith('px')) ? currentRight : GSRS_App.DEFAULT_SETTINGS.uiPanelRight;
GSRS_App.currentSettings.uiPanelLeft = 'auto';
GM_setValue('gsrs_uiPanelRight', GSRS_App.currentSettings.uiPanelRight);
GM_setValue('gsrs_uiPanelLeft', 'auto');
} else {
GSRS_App.currentSettings.uiPanelRight = GSRS_App.DEFAULT_SETTINGS.uiPanelRight;
GSRS_App.currentSettings.uiPanelLeft = 'auto';
GM_setValue('gsrs_uiPanelRight', GSRS_App.currentSettings.uiPanelRight);
GM_setValue('gsrs_uiPanelLeft', 'auto');
}
}
GM_setValue('gsrs_lastViewMode', GSRS_App.state.currentViewMode);
},
updateInputs: function() {
const elements = GSRS_App.uiElements.settingsDOMElements; if (!elements || Object.keys(elements).length === 0) { return; }
if (elements.titleInput) elements.titleInput.value = GSRS_App.currentSettings.titleSelector;
if (elements.observerInput) elements.observerInput.value = GSRS_App.currentSettings.observerTargetSelector;
if (elements.debugCheckbox) elements.debugCheckbox.checked = GSRS_App.currentSettings.debugMode;
if (elements.highlightCheckbox) elements.highlightCheckbox.checked = GSRS_App.currentSettings.highlightParsed;
if (elements.highlightListItemOnPageCheckbox) elements.highlightListItemOnPageCheckbox.checked = GSRS_App.currentSettings.highlightListItemOnPage;
if (elements.showPreviewInListModeCheckbox) elements.showPreviewInListModeCheckbox.checked = GSRS_App.currentSettings.showPreviewInListMode;
if (elements.showFilterInputAreaCheckbox) elements.showFilterInputAreaCheckbox.checked = GSRS_App.currentSettings.showFilterInputArea;
if (elements.showDownloadActionsAreaCheckbox) elements.showDownloadActionsAreaCheckbox.checked = GSRS_App.currentSettings.showDownloadActionsArea;
if (elements.darkModeCheckbox) elements.darkModeCheckbox.checked = GSRS_App.currentSettings.darkMode;
if (elements.fetchTitleCheckbox) elements.fetchTitleCheckbox.checked = GSRS_App.currentSettings.fetchTitle;
if (elements.fetchUrlCheckbox) elements.fetchUrlCheckbox.checked = GSRS_App.currentSettings.fetchUrl;
if (elements.fetchSiteNameCheckbox) elements.fetchSiteNameCheckbox.checked = GSRS_App.currentSettings.fetchSiteName;
if (elements.fetchDescriptionCheckbox) elements.fetchDescriptionCheckbox.checked = GSRS_App.currentSettings.fetchDescription;
if (elements.fetchDescriptionKeywordsCheckbox) elements.fetchDescriptionKeywordsCheckbox.checked = GSRS_App.currentSettings.fetchDescriptionKeywords;
if (elements.fetchDateInfoCheckbox) elements.fetchDateInfoCheckbox.checked = GSRS_App.currentSettings.fetchDateInfo;
if (elements.fetchBreadcrumbsCheckbox) elements.fetchBreadcrumbsCheckbox.checked = GSRS_App.currentSettings.fetchBreadcrumbs;
if (elements.decodeUrlsCheckbox) elements.decodeUrlsCheckbox.checked = GSRS_App.currentSettings.decodeUrlsToReadable;
if (elements.hideFieldsCheckbox) elements.hideFieldsCheckbox.checked = GSRS_App.currentSettings.hideDisabledFetchFields;
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
if (key.startsWith('exportCsvMd')) {
const checkboxId = `gsrs-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}-checkbox`;
const checkbox = document.getElementById(checkboxId);
if (checkbox) { checkbox.checked = GSRS_App.currentSettings[key]; }
}
});
const fetchDescCheckbox = elements.fetchDescriptionCheckbox;
const fetchKeywordsContainer = document.getElementById('gsrs-fetch-keywords-container');
if (fetchDescCheckbox && fetchKeywordsContainer) {
fetchKeywordsContainer.style.display = fetchDescCheckbox.checked ? 'block' : 'none';
}
},
save: function() {
const elements = GSRS_App.uiElements.settingsDOMElements; if (!elements || Object.keys(elements).length === 0) { GSRS_App.uiManager.showUIMessage('Error: Settings DOM elements not found.', 'error'); return; }
const { titleInput, observerInput, debugCheckbox, highlightCheckbox, highlightListItemOnPageCheckbox, showPreviewInListModeCheckbox, showFilterInputAreaCheckbox, showDownloadActionsAreaCheckbox, darkModeCheckbox, fetchTitleCheckbox, fetchUrlCheckbox, fetchSiteNameCheckbox, fetchDescriptionCheckbox, fetchDescriptionKeywordsCheckbox, fetchBreadcrumbsCheckbox, decodeUrlsCheckbox, fetchDateInfoCheckbox, hideFieldsCheckbox } = elements;
if (!titleInput || !observerInput || !debugCheckbox || !highlightCheckbox || !highlightListItemOnPageCheckbox || !showPreviewInListModeCheckbox || !showFilterInputAreaCheckbox || !showDownloadActionsAreaCheckbox || !darkModeCheckbox || !fetchTitleCheckbox || !fetchUrlCheckbox || !fetchSiteNameCheckbox || !fetchDescriptionCheckbox || !fetchDescriptionKeywordsCheckbox || !fetchBreadcrumbsCheckbox || !decodeUrlsCheckbox || !fetchDateInfoCheckbox || !hideFieldsCheckbox ) { GSRS_App.uiManager.showUIMessage('Error: One or more core settings input elements are missing.', 'error'); if(GSRS_App.currentSettings.debugMode) console.error("GSRS SaveSettings: Missing core DOM elements", elements); return; }
const newTitleSelector = titleInput.value.trim();
if (!newTitleSelector || !isValidSelector(newTitleSelector)) {
GSRS_App.uiManager.showUIMessage(`Invalid or empty Title Selector: "${newTitleSelector.substring(0,50)}...". Using default.`, 'error', 7000);
GSRS_App.currentSettings.titleSelector = GSRS_App.DEFAULT_SETTINGS.titleSelector;
} else {
GSRS_App.currentSettings.titleSelector = newTitleSelector;
}
const newObserverSelector = observerInput.value.trim();
if (newObserverSelector && newObserverSelector.indexOf(',') === -1 && !isValidSelector(newObserverSelector)) {
GSRS_App.uiManager.showUIMessage(`Invalid Observer Target Selector (Legacy): ${newObserverSelector.substring(0,50)}...`, 'error', 5000); return;
}
if (newObserverSelector && newObserverSelector.includes(',')) {
const selectors = newObserverSelector.split(',').map(s => s.trim());
for (const sel of selectors) {
if (!sel || !isValidSelector(sel)) { GSRS_App.uiManager.showUIMessage(`Invalid selector in Observer Targets list (Legacy): ${sel.substring(0,50)}...`, 'error', 5000); return; }
}
}
GSRS_App.currentSettings.observerTargetSelector = newObserverSelector || GSRS_App.DEFAULT_SETTINGS.observerTargetSelector;
GSRS_App.currentSettings.debugMode = debugCheckbox.checked;
GSRS_App.currentSettings.highlightParsed = highlightCheckbox.checked;
GSRS_App.currentSettings.highlightListItemOnPage = highlightListItemOnPageCheckbox.checked;
GSRS_App.currentSettings.showPreviewInListMode = showPreviewInListModeCheckbox.checked;
GSRS_App.currentSettings.showFilterInputArea = showFilterInputAreaCheckbox.checked;
GSRS_App.currentSettings.showDownloadActionsArea = showDownloadActionsAreaCheckbox.checked;
GSRS_App.currentSettings.fetchTitle = fetchTitleCheckbox.checked;
GSRS_App.currentSettings.fetchUrl = fetchUrlCheckbox.checked;
GSRS_App.currentSettings.fetchSiteName = fetchSiteNameCheckbox.checked;
GSRS_App.currentSettings.fetchDescription = fetchDescriptionCheckbox.checked;
GSRS_App.currentSettings.fetchDescriptionKeywords = fetchDescriptionKeywordsCheckbox.checked;
GSRS_App.currentSettings.fetchBreadcrumbs = fetchBreadcrumbsCheckbox.checked;
GSRS_App.currentSettings.decodeUrlsToReadable = decodeUrlsCheckbox.checked;
GSRS_App.currentSettings.fetchDateInfo = fetchDateInfoCheckbox.checked;
GSRS_App.currentSettings.hideDisabledFetchFields = hideFieldsCheckbox.checked;
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
if (key === 'darkMode') {
GM_setValue(`gsrs_${key}`, darkModeCheckbox.checked);
GSRS_App.currentSettings[key] = darkModeCheckbox.checked;
} else if (GSRS_App.currentSettings.hasOwnProperty(key)) {
GM_setValue(`gsrs_${key}`, GSRS_App.currentSettings[key]);
}
});
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
if (key.startsWith('exportCsvMd')) {
const checkboxId = `gsrs-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}-checkbox`;
const checkbox = document.getElementById(checkboxId);
if (checkbox) {
GSRS_App.currentSettings[key] = checkbox.checked;
GM_setValue(`gsrs_${key}`, GSRS_App.currentSettings[key]);
} else if (GSRS_App.currentSettings.debugMode) {
console.warn(`GSRS SaveSettings: Checkbox with ID ${checkboxId} for export setting ${key} not found.`);
}
}
});
titleInput.value = GSRS_App.currentSettings.titleSelector;
observerInput.value = GSRS_App.currentSettings.observerTargetSelector;
const warningP = document.querySelector('#gsrs-settings-panel .gsrs-settings-warning');
if (warningP) { warningP.innerHTML = `Extraction uses title selector '<code>${GSRS_App.currentSettings.titleSelector}</code>' to find parent blocks.`; }
if (GSRS_App.uiElements.resultPreviewArea) {
GSRS_App.uiElements.resultPreviewArea.style.display = (GSRS_App.currentSettings.showPreviewInListMode && GSRS_App.state.currentViewMode === 'list') ? 'block' : 'none';
}
const filterContainerEl = document.querySelector('.gsrs-filter-container');
if (filterContainerEl) { filterContainerEl.style.display = GSRS_App.currentSettings.showFilterInputArea ? 'flex' : 'none'; }
const downloadOptionsEl = document.querySelector('.gsrs-copy-download-options');
const downloadBarEl = document.querySelector('.gsrs-action-button-bar');
if (downloadOptionsEl) downloadOptionsEl.style.display = GSRS_App.currentSettings.showDownloadActionsArea ? 'block' : 'none';
if (downloadBarEl) downloadBarEl.style.display = GSRS_App.currentSettings.showDownloadActionsArea ? 'flex' : 'none';
if (GSRS_App.state.currentViewMode === 'list') {
GSRS_App.uiManager.toggleViewMode('list');
}
GSRS_App.uiManager.toggleDarkMode(darkModeCheckbox.checked);
GSRS_App.uiManager.showUIMessage('Settings saved! Re-scrape if needed.', 'success');
if (GSRS_App.currentSettings.debugMode) console.log("GSRS: Settings saved.");
if (GSRS_App.observer) { GSRS_App.observer.disconnect(); GSRS_App.uiManager.updateObserverStatus(false); GSRS_App.observer = null; }
GSRS_App.uiManager.toggleSettingsView(false);
},
resetToDefaults: function() {
if (confirm("Reset ALL settings to defaults (selectors, fetching, display, UI position)? Filter term won't change.")) {
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
GSRS_App.currentSettings[key] = GSRS_App.DEFAULT_SETTINGS[key];
GM_setValue(`gsrs_${key}`, GSRS_App.DEFAULT_SETTINGS[key]);
});
GSRS_App.currentSettings.uiPanelTop = GSRS_App.DEFAULT_SETTINGS.uiPanelTop;
GSRS_App.currentSettings.uiPanelLeft = GSRS_App.DEFAULT_SETTINGS.uiPanelLeft;
GSRS_App.currentSettings.uiPanelRight = GSRS_App.DEFAULT_SETTINGS.uiPanelRight;
GM_setValue('gsrs_uiPanelTop', GSRS_App.currentSettings.uiPanelTop);
GM_setValue('gsrs_uiPanelLeft', GSRS_App.currentSettings.uiPanelLeft);
GM_setValue('gsrs_uiPanelRight', GSRS_App.currentSettings.uiPanelRight);
const uiContainer = GSRS_App.uiElements.uiContainer;
if (uiContainer) {
uiContainer.style.top = GSRS_App.currentSettings.uiPanelTop;
uiContainer.style.left = GSRS_App.currentSettings.uiPanelLeft;
uiContainer.style.right = GSRS_App.currentSettings.uiPanelRight;
uiContainer.style.width = GSRS_App.DEFAULT_SETTINGS.uiPanelWidth;
}
this.updateInputs();
const warningP = document.querySelector('#gsrs-settings-panel .gsrs-settings-warning');
if (warningP) { warningP.innerHTML = `Extraction uses title selector '<code>${GSRS_App.DEFAULT_SETTINGS.titleSelector}</code>' to find parent blocks.`; }
if (GSRS_App.uiElements.resultPreviewArea) {
GSRS_App.uiElements.resultPreviewArea.style.display = (GSRS_App.DEFAULT_SETTINGS.showPreviewInListMode && GSRS_App.state.currentViewMode === 'list') ? 'block' : 'none';
}
const filterContainerEl = document.querySelector('.gsrs-filter-container');
if (filterContainerEl) { filterContainerEl.style.display = GSRS_App.DEFAULT_SETTINGS.showFilterInputArea ? 'flex' : 'none'; }
const downloadOptionsEl = document.querySelector('.gsrs-copy-download-options');
const downloadBarEl = document.querySelector('.gsrs-action-button-bar');
if (downloadOptionsEl) downloadOptionsEl.style.display = GSRS_App.DEFAULT_SETTINGS.showDownloadActionsArea ? 'block' : 'none';
if (downloadBarEl) downloadBarEl.style.display = GSRS_App.DEFAULT_SETTINGS.showDownloadActionsArea ? 'flex' : 'none';
if (GSRS_App.state.currentViewMode === 'list') {
GSRS_App.uiManager.toggleViewMode('list');
}
GSRS_App.uiManager.toggleDarkMode(GSRS_App.currentSettings.darkMode);
GSRS_App.uiManager.showUIMessage('All settings reset to defaults. Re-scrape if needed.', 'success');
if (GSRS_App.currentSettings.debugMode) console.log("GSRS: All settings reset.");
if (GSRS_App.observer) { GSRS_App.observer.disconnect(); GSRS_App.uiManager.updateObserverStatus(false); GSRS_App.observer = null;}
GSRS_App.uiManager.toggleSettingsView(false);
}
}
};
// --- UI Manager ---
GSRS_App.uiManager = {
create: function() {
if (document.getElementById('gsrs-ui-container')) return;
GSRS_App.uiElements.uiContainer = document.createElement('div');
GSRS_App.uiElements.uiContainer.id = 'gsrs-ui-container';
if (GSRS_App.currentSettings.darkMode) { GSRS_App.uiElements.uiContainer.classList.add('gsrs-dark-theme'); }
GSRS_App.uiElements.uiContainer.style.top = GSRS_App.currentSettings.uiPanelTop || GSRS_App.DEFAULT_SETTINGS.uiPanelTop;
if (GSRS_App.currentSettings.uiPanelLeft && GSRS_App.currentSettings.uiPanelLeft !== 'auto') {
GSRS_App.uiElements.uiContainer.style.left = GSRS_App.currentSettings.uiPanelLeft;
GSRS_App.uiElements.uiContainer.style.right = 'auto';
} else if (GSRS_App.currentSettings.uiPanelRight && GSRS_App.currentSettings.uiPanelRight !== 'auto') {
GSRS_App.uiElements.uiContainer.style.right = GSRS_App.currentSettings.uiPanelRight;
GSRS_App.uiElements.uiContainer.style.left = 'auto';
} else {
GSRS_App.uiElements.uiContainer.style.left = GSRS_App.DEFAULT_SETTINGS.uiPanelLeft;
GSRS_App.uiElements.uiContainer.style.right = GSRS_App.DEFAULT_SETTINGS.uiPanelRight;
}
if (GSRS_App.currentSettings.uiSettingsVisible) GSRS_App.uiElements.uiContainer.classList.add('gsrs-settings-view-active');
const titleBar = this.createTitleBar();
GSRS_App.uiElements.uiContainer.appendChild(titleBar);
const contentWrapper = document.createElement('div');
contentWrapper.id = 'gsrs-content-wrapper';
contentWrapper.style.display = GSRS_App.currentSettings.uiContentVisible ? 'flex' : 'none';
if (!GSRS_App.currentSettings.uiContentVisible) { GSRS_App.uiElements.uiContainer.classList.add('gsrs-minimized');}
contentWrapper.appendChild(this.createMainActions());
contentWrapper.appendChild(this.createFilterArea());
contentWrapper.appendChild(this.createResultsCountAndViews());
contentWrapper.appendChild(this.createResultsViewAreaContainer());
GSRS_App.uiElements.uiMessageDiv = document.createElement('div');
GSRS_App.uiElements.uiMessageDiv.id = 'gsrs-ui-message';
contentWrapper.appendChild(GSRS_App.uiElements.uiMessageDiv);
contentWrapper.appendChild(this.createCopyDownloadOptions());
contentWrapper.appendChild(this.createActionButtonBar());
GSRS_App.uiElements.settingsPanel = this.createSettingsPanel();
contentWrapper.appendChild(GSRS_App.uiElements.settingsPanel);
GSRS_App.uiElements.uiContainer.appendChild(contentWrapper);
GSRS_App.uiElements.contextMenuElement = this.createContextMenu();
document.body.appendChild(GSRS_App.uiElements.contextMenuElement);
document.body.appendChild(GSRS_App.uiElements.uiContainer);
this.populateSettingsDOMElements();
this.attachSettingsPanelEvents();
this.attachActionLinkEvents();
this.attachMinimizeToggle(titleBar.querySelector('#gsrs-minimize-btn'), contentWrapper);
this.makeDraggable(GSRS_App.uiElements.uiContainer, titleBar);
this.addStyles();
},
createTitleBar: function() {
const titleBar = document.createElement('div'); titleBar.id = 'gsrs-title-bar';
const settingsToggleTitleBar = document.createElement('button'); settingsToggleTitleBar.id = 'gsrs-settings-toggle-titlebar'; settingsToggleTitleBar.innerHTML = '⚙️'; settingsToggleTitleBar.title = 'Settings'; settingsToggleTitleBar.addEventListener('click', () => this.toggleSettingsView()); titleBar.appendChild(settingsToggleTitleBar);
const titleBarText = document.createElement('span'); titleBarText.id = 'gsrs-title-bar-text'; titleBarText.textContent = GSRS_App.state.originalTitleBarText; titleBar.appendChild(titleBarText);
const titleBarControls = document.createElement('div'); titleBarControls.id = 'gsrs-title-bar-controls';
GSRS_App.uiElements.observerStatusSpan = document.createElement('span'); GSRS_App.uiElements.observerStatusSpan.id = 'gsrs-observer-status';
GSRS_App.uiElements.observerStatusSpan.innerHTML = '○'; // Default to inactive
GSRS_App.uiElements.observerStatusSpan.title = 'Dynamic loading detection inactive'; // Title updated
GSRS_App.uiElements.observerStatusSpan.classList.add('gsrs-obs-inactive'); titleBarControls.appendChild(GSRS_App.uiElements.observerStatusSpan);
const maximizeButton = document.createElement('button'); maximizeButton.id = 'gsrs-maximize-btn'; maximizeButton.innerHTML = '<span class="icon">🗖</span>'; maximizeButton.title = 'Maximize Panel'; maximizeButton.addEventListener('click', toggleMaximizePanel); titleBarControls.appendChild(maximizeButton);
const minimizeButton = document.createElement('button'); minimizeButton.id = 'gsrs-minimize-btn'; minimizeButton.textContent = '-'; minimizeButton.title = 'Minimize/Restore Panel'; titleBarControls.appendChild(minimizeButton);
titleBar.appendChild(titleBarControls);
return titleBar;
},
attachMinimizeToggle: function(minimizeButton, contentWrapper) {
minimizeButton.addEventListener('click', () => {
const titleBarTextEl = document.getElementById('gsrs-title-bar-text');
if (!contentWrapper || !titleBarTextEl) return;
const isCurrentlyHidden = getComputedStyle(contentWrapper).display === 'none';
const newVisibility = isCurrentlyHidden;
contentWrapper.style.display = newVisibility ? 'flex' : 'none';
GSRS_App.uiElements.uiContainer.classList.toggle('gsrs-minimized', !newVisibility);
minimizeButton.textContent = newVisibility ? '-' : '+';
GSRS_App.currentSettings.uiContentVisible = newVisibility;
titleBarTextEl.textContent = GSRS_App.state.originalTitleBarText;
if (!newVisibility && GSRS_App.uiElements.uiContainer.classList.contains('gsrs-settings-view-active')) {
this.toggleSettingsView(false);
}
GSRS_App.settingsManager.saveUIPrefs();
if (newVisibility && !GSRS_App.uiElements.uiContainer.classList.contains('gsrs-settings-view-active')) {
contentWrapper.style.display = 'none'; void contentWrapper.offsetHeight; contentWrapper.style.display = 'flex';
}
});
if(!GSRS_App.currentSettings.uiContentVisible) {
minimizeButton.textContent = '+';
const titleBarTextEl = document.getElementById('gsrs-title-bar-text');
if (titleBarTextEl) titleBarTextEl.textContent = GSRS_App.state.originalTitleBarText;
}
},
createMainActions: function() {
const mainActionsDiv = document.createElement('div'); mainActionsDiv.className = 'gsrs-main-actions';
const startButton = document.createElement('button'); startButton.id = 'gsrs-start-btn'; startButton.className = 'gsrs-button';
startButton.textContent = 'Scrape Page'; // Text reflects no ongoing observation
startButton.addEventListener('click', handleStartParse); mainActionsDiv.appendChild(startButton);
const clearButton = document.createElement('button'); clearButton.id = 'gsrs-clear-btn'; clearButton.className = 'gsrs-button'; clearButton.innerHTML = '<span class="icon">🗑️</span>'; clearButton.title = 'Clear Results'; clearButton.addEventListener('click', handleClearResults); mainActionsDiv.appendChild(clearButton);
return mainActionsDiv;
},
createFilterArea: function() {
const filterContainerEl = document.createElement('div'); filterContainerEl.className = 'gsrs-filter-container';
filterContainerEl.style.display = GSRS_App.currentSettings.showFilterInputArea ? 'flex' : 'none';
GSRS_App.uiElements.filterInput = document.createElement('input'); GSRS_App.uiElements.filterInput.type = 'text'; GSRS_App.uiElements.filterInput.id = 'gsrs-filter-input'; GSRS_App.uiElements.filterInput.placeholder = 'Filter results by keyword...';
GSRS_App.uiElements.filterInput.value = GSRS_App.currentSettings.lastFilterTerm;
GSRS_App.uiElements.filterInput.addEventListener('input', handleFilterResults);
filterContainerEl.appendChild(GSRS_App.uiElements.filterInput);
const clearFilterButton = document.createElement('button'); clearFilterButton.id = 'gsrs-clear-filter-btn'; clearFilterButton.className = 'gsrs-button'; clearFilterButton.innerHTML = 'X'; clearFilterButton.title = 'Clear Filter';
clearFilterButton.addEventListener('click', () => { if (GSRS_App.uiElements.filterInput) GSRS_App.uiElements.filterInput.value = ''; handleFilterResults(); });
filterContainerEl.appendChild(clearFilterButton);
if (GSRS_App.currentSettings.lastFilterTerm && GSRS_App.currentSettings.lastFilterTerm.trim() !== '') {
GSRS_App.uiElements.filterInput.classList.add('gsrs-filter-active');
}
return filterContainerEl;
},
createResultsCountAndViews: function() {
const resultsCountWrapper = document.createElement('div'); resultsCountWrapper.id = 'gsrs-results-count-wrapper';
GSRS_App.uiElements.resultsCountSpan = document.createElement('span'); GSRS_App.uiElements.resultsCountSpan.id = 'gsrs-results-count'; GSRS_App.uiElements.resultsCountSpan.textContent = 'Results: 0'; resultsCountWrapper.appendChild(GSRS_App.uiElements.resultsCountSpan);
const viewToggleButtons = document.createElement('div'); viewToggleButtons.className = 'gsrs-view-toggle-buttons';
const jsonViewBtn = document.createElement('button'); jsonViewBtn.id = 'gsrs-view-toggle-json'; jsonViewBtn.textContent = '📜 JSON'; jsonViewBtn.title = "View as JSON";
const listViewBtn = document.createElement('button'); listViewBtn.id = 'gsrs-view-toggle-list'; listViewBtn.textContent = '📄 List'; listViewBtn.title = "View as List & Preview";
jsonViewBtn.addEventListener('click', () => this.toggleViewMode('json'));
listViewBtn.addEventListener('click', () => this.toggleViewMode('list'));
viewToggleButtons.appendChild(jsonViewBtn); viewToggleButtons.appendChild(listViewBtn);
resultsCountWrapper.appendChild(viewToggleButtons);
return resultsCountWrapper;
},
createResultsViewAreaContainer: function() {
const resultsViewAreaContainer = document.createElement('div'); resultsViewAreaContainer.id = 'gsrs-results-view-area';
GSRS_App.uiElements.resultsTextArea = document.createElement('textarea'); GSRS_App.uiElements.resultsTextArea.id = 'gsrs-results-area'; GSRS_App.uiElements.resultsTextArea.placeholder = 'Scraped results will appear here...'; GSRS_App.uiElements.resultsTextArea.readOnly = true; resultsViewAreaContainer.appendChild(GSRS_App.uiElements.resultsTextArea);
GSRS_App.uiElements.resultsListContainer = document.createElement('div'); GSRS_App.uiElements.resultsListContainer.id = 'gsrs-results-list-container'; resultsViewAreaContainer.appendChild(GSRS_App.uiElements.resultsListContainer);
GSRS_App.uiElements.resultPreviewArea = document.createElement('div'); GSRS_App.uiElements.resultPreviewArea.id = 'gsrs-result-preview-area'; GSRS_App.uiElements.resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 10px;">Click an item from the list to see details.</p>'; resultsViewAreaContainer.appendChild(GSRS_App.uiElements.resultPreviewArea);
return resultsViewAreaContainer;
},
createCopyDownloadOptions: function() {
const copyDownloadOptionsDiv = document.createElement('div'); copyDownloadOptionsDiv.className = 'gsrs-copy-download-options';
copyDownloadOptionsDiv.style.display = GSRS_App.currentSettings.showDownloadActionsArea ? 'block' : 'none';
copyDownloadOptionsDiv.innerHTML = `<span>Action Target: </span><label><input type="radio" name="gsrsCopyDownloadTarget" value="current" checked> Current View</label><label><input type="radio" name="gsrsCopyDownloadTarget" value="all"> All Results</label>`;
copyDownloadOptionsDiv.querySelectorAll('input[name="gsrsCopyDownloadTarget"]').forEach(radio => {
radio.addEventListener('change', (event) => {
GSRS_App.state.copyDownloadTarget = event.target.value;
if(GSRS_App.currentSettings.debugMode) console.log("GSRS Debug: Copy/Download target set to:", GSRS_App.state.copyDownloadTarget);
this.updateActionLinkTitles();
updateActionButtonsState();
});
});
const currentTargetRadio = copyDownloadOptionsDiv.querySelector(`input[value="${GSRS_App.state.copyDownloadTarget}"]`);
if (currentTargetRadio) currentTargetRadio.checked = true;
return copyDownloadOptionsDiv;
},
createActionButtonBar: function() {
const actionButtonBar = document.createElement('div'); actionButtonBar.id = 'gsrs-action-button-bar'; actionButtonBar.className = 'gsrs-action-button-bar';
actionButtonBar.style.display = GSRS_App.currentSettings.showDownloadActionsArea ? 'flex' : 'none';
const copyActionSet = document.createElement('div'); copyActionSet.className = 'gsrs-action-set';
const copyIcon = document.createElement('span'); copyIcon.className = 'icon gsrs-action-icon'; copyIcon.innerHTML = '📋'; copyActionSet.appendChild(copyIcon);
['json', 'urls', 'md'].forEach((format, index, arr) => {
const link = document.createElement('span'); link.className = 'gsrs-action-format-link'; link.dataset.action = 'copy'; link.dataset.format = format; link.textContent = format.toUpperCase();
if (format === 'urls') link.textContent = 'URLs'; if (format === 'md') link.textContent = 'MD';
copyActionSet.appendChild(link); if (index < arr.length - 1) copyActionSet.appendChild(document.createTextNode(', '));
});
actionButtonBar.appendChild(copyActionSet);
const downloadActionSet = document.createElement('div'); downloadActionSet.className = 'gsrs-action-set';
const downloadIcon = document.createElement('span'); downloadIcon.className = 'icon gsrs-action-icon'; downloadIcon.innerHTML = '⬇️'; downloadActionSet.appendChild(downloadIcon);
['json', 'csv', 'urls', 'md'].forEach((format, index, arr) => {
const link = document.createElement('span'); link.className = 'gsrs-action-format-link'; link.dataset.action = 'download'; link.dataset.format = format; link.textContent = format.toUpperCase();
if (format === 'urls') link.textContent = 'URLs.txt'; if (format === 'md') link.textContent = 'MD';
downloadActionSet.appendChild(link); if (index < arr.length - 1) downloadActionSet.appendChild(document.createTextNode(', '));
});
actionButtonBar.appendChild(downloadActionSet);
this.updateActionLinkTitles();
return actionButtonBar;
},
updateActionLinkTitles: function() {
const actionButtonBar = document.getElementById('gsrs-action-button-bar'); if (!actionButtonBar) return;
const targetText = GSRS_App.state.copyDownloadTarget === 'current' ? '(Current View)' : '(All Results)';
actionButtonBar.querySelectorAll('.gsrs-action-format-link').forEach(link => {
const action = link.dataset.action; const format = link.dataset.format.toUpperCase();
let baseTitle = `${action.charAt(0).toUpperCase() + action.slice(1)} ${format === 'URLS' ? 'URLs' : format.replace('URLS.TXT', 'URLs.txt')}`;
if (format === 'MD') baseTitle = `${action.charAt(0).toUpperCase() + action.slice(1)} MD`;
link.title = `${baseTitle} ${targetText}`;
});
},
createSettingsPanel: function() {
const settingsPanel = document.createElement('div'); settingsPanel.id = 'gsrs-settings-panel';
let exportOptionsHTML = '<div class="gsrs-export-options-grid">';
const exportFieldLabels = { Position: "Position", Title: "Title", Url: "URL", SiteName: "Site Name", Breadcrumbs: "Breadcrumbs", Description: "Description", HighlightedSnippets: "Keywords", OriginalDateText: "Original Date", ParsedDateISO: "Parsed Date" };
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
if (key.startsWith('exportCsvMd')) {
const fieldName = key.substring('exportCsvMd'.length);
const checkboxId = `gsrs-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}-checkbox`;
const labelText = exportFieldLabels[fieldName] || fieldName;
exportOptionsHTML += `<div><input type="checkbox" id="${checkboxId}"><label for="${checkboxId}" style="font-weight:normal; cursor:pointer;">${labelText}</label></div>`;
}
});
exportOptionsHTML += '</div>';
settingsPanel.innerHTML = ` <details open> <summary>Selectors</summary> <div> <div class="gsrs-setting-item"> <label for="gsrs-input-title-selector">Title Element Selector:</label> <div class="gsrs-setting-input-group"> <input type="text" id="gsrs-input-title-selector" title="CSS selector for result titles."> <button class="gsrs-test-selector-btn" data-input-id="gsrs-input-title-selector" data-result-id="gsrs-title-test-result" data-preview-id="gsrs-title-test-preview">Test</button> <span class="gsrs-test-result-span" id="gsrs-title-test-result"></span> </div> <div class="gsrs-selector-test-preview" id="gsrs-title-test-preview"></div> </div> <div class="gsrs-setting-item"> <label for="gsrs-input-observer-selector">Observer Target Selector(s) (Legacy/Informational):</label> <div class="gsrs-setting-input-group"> <input type="text" id="gsrs-input-observer-selector" title="Legacy: Informational only, not used for active observation."> <button class="gsrs-test-selector-btn" data-input-id="gsrs-input-observer-selector" data-result-id="gsrs-observer-test-result" data-preview-id="gsrs-observer-test-preview">Test</button> <span class="gsrs-test-result-span" id="gsrs-observer-test-result"></span> </div> <div class="gsrs-selector-test-preview" id="gsrs-observer-test-preview"></div> </div> </div> </details> <details> <summary>Data Fetching & Processing</summary> <div> <div class="gsrs-setting-input-group" style="flex-direction: column; align-items: flex-start;"> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-title-checkbox"><label for="gsrs-fetch-title-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Title</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-url-checkbox"><label for="gsrs-fetch-url-checkbox" style="font-weight:normal; cursor:pointer;">Fetch URL</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-sitename-checkbox"><label for="gsrs-fetch-sitename-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Site Name</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-breadcrumbs-checkbox"><label for="gsrs-fetch-breadcrumbs-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Breadcrumbs</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-description-checkbox"><label for="gsrs-fetch-description-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Description</label></div> <div style="margin-left: 20px; margin-bottom: 5px; display: none;" id="gsrs-fetch-keywords-container"> <input type="checkbox" id="gsrs-fetch-description-keywords-checkbox"><label for="gsrs-fetch-description-keywords-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Highlighted Keywords in Description</label> </div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-fetch-dateinfo-checkbox"><label for="gsrs-fetch-dateinfo-checkbox" style="font-weight:normal; cursor:pointer;">Fetch Date Information (from Description)</label></div> <div><input type="checkbox" id="gsrs-decode-urls-checkbox"><label for="gsrs-decode-urls-checkbox" style="font-weight:normal; cursor:pointer;">Decode URLs to Readable Format</label></div> </div> </div> </details> <details> <summary>CSV/Markdown Export Fields</summary> <div> ${exportOptionsHTML} </div> </details> <details> <summary>Interface Display Options</summary> <div> <div class="gsrs-setting-input-group" style="flex-direction: column; align-items: flex-start;"> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-dark-mode-checkbox"><label for="gsrs-dark-mode-checkbox" style="font-weight:normal; cursor:pointer;">Enable Dark Mode</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-show-preview-list-mode-checkbox"><label for="gsrs-show-preview-list-mode-checkbox" style="font-weight:normal; cursor:pointer;">Show preview in List mode</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-show-filter-area-checkbox"><label for="gsrs-show-filter-area-checkbox" style="font-weight:normal; cursor:pointer;">Show filter area</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-show-download-area-checkbox"><label for="gsrs-show-download-area-checkbox" style="font-weight:normal; cursor:pointer;">Show download actions area</label></div> </div> </div> </details> <details> <summary>Highlighting Options</summary> <div> <div class="gsrs-setting-input-group" style="flex-direction: column; align-items: flex-start;"> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-highlight-parsed-checkbox"><label for="gsrs-highlight-parsed-checkbox" style="font-weight:normal; cursor:pointer;">Highlight Scraped Results (temporary, during scrape)</label></div> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-highlight-list-item-on-page-checkbox"><label for="gsrs-highlight-list-item-on-page-checkbox" style="font-weight:normal; cursor:pointer;">Highlight selected list item on page</label></div> </div> </div> </details> <details> <summary>Debug & Output Options</summary> <div> <div class="gsrs-setting-input-group" style="flex-direction: column; align-items: flex-start;"> <div style="margin-bottom: 5px;"><input type="checkbox" id="gsrs-debug-mode-checkbox"><label for="gsrs-debug-mode-checkbox" style="font-weight:normal; cursor:pointer;">Enable Debug Mode Logging</label></div> <div><input type="checkbox" id="gsrs-hide-fields-checkbox"><label for="gsrs-hide-fields-checkbox" style="font-weight:normal; cursor:pointer;">Hide non-fetched fields in output</label></div> </div> </div> </details> <div class="gsrs-button-group" style="margin-top: 15px;"> <button class="gsrs-button" id="gsrs-btn-save-settings">Save Settings</button> <button class="gsrs-button" id="gsrs-btn-reset-settings">Reset to Defaults</button> </div> <p class="gsrs-settings-warning">Extraction uses title selector '<code>${GSRS_App.DEFAULT_SETTINGS.titleSelector}</code>' to find parent blocks.</p> `;
return settingsPanel;
},
populateSettingsDOMElements: function() {
GSRS_App.uiElements.settingsDOMElements = { titleInput: document.getElementById('gsrs-input-title-selector'), observerInput: document.getElementById('gsrs-input-observer-selector'), debugCheckbox: document.getElementById('gsrs-debug-mode-checkbox'), highlightCheckbox: document.getElementById('gsrs-highlight-parsed-checkbox'), highlightListItemOnPageCheckbox: document.getElementById('gsrs-highlight-list-item-on-page-checkbox'), showPreviewInListModeCheckbox: document.getElementById('gsrs-show-preview-list-mode-checkbox'), showFilterInputAreaCheckbox: document.getElementById('gsrs-show-filter-area-checkbox'), showDownloadActionsAreaCheckbox: document.getElementById('gsrs-show-download-area-checkbox'), darkModeCheckbox: document.getElementById('gsrs-dark-mode-checkbox'), fetchTitleCheckbox: document.getElementById('gsrs-fetch-title-checkbox'), fetchUrlCheckbox: document.getElementById('gsrs-fetch-url-checkbox'), fetchSiteNameCheckbox: document.getElementById('gsrs-fetch-sitename-checkbox'), fetchDescriptionCheckbox: document.getElementById('gsrs-fetch-description-checkbox'), fetchDescriptionKeywordsCheckbox: document.getElementById('gsrs-fetch-description-keywords-checkbox'), fetchDateInfoCheckbox: document.getElementById('gsrs-fetch-dateinfo-checkbox'), fetchBreadcrumbsCheckbox: document.getElementById('gsrs-fetch-breadcrumbs-checkbox'), decodeUrlsCheckbox: document.getElementById('gsrs-decode-urls-checkbox'), hideFieldsCheckbox: document.getElementById('gsrs-hide-fields-checkbox'), saveButton: document.getElementById('gsrs-btn-save-settings'), resetButton: document.getElementById('gsrs-btn-reset-settings'), };
Object.keys(GSRS_App.DEFAULT_SETTINGS).forEach(key => {
if (key.startsWith('exportCsvMd')) {
const checkboxId = `gsrs-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}-checkbox`;
GSRS_App.uiElements.settingsDOMElements[key + 'Checkbox'] = document.getElementById(checkboxId);
}
});
},
attachSettingsPanelEvents: function() {
const elements = GSRS_App.uiElements.settingsDOMElements;
if (elements.saveButton) elements.saveButton.addEventListener('click', () => GSRS_App.settingsManager.save());
if (elements.resetButton) elements.resetButton.addEventListener('click', () => GSRS_App.settingsManager.resetToDefaults());
if (elements.darkModeCheckbox) { elements.darkModeCheckbox.addEventListener('change', (event) => { this.toggleDarkMode(event.target.checked); }); }
const settingsPanel = GSRS_App.uiElements.settingsPanel;
if(settingsPanel) {
settingsPanel.querySelectorAll('.gsrs-test-selector-btn').forEach(btn => { btn.addEventListener('click', () => testSelector(btn.dataset.inputId, btn.dataset.resultId, btn.dataset.previewId)); });
const fetchDescCheckbox = elements.fetchDescriptionCheckbox;
const fetchKeywordsContainer = settingsPanel.querySelector('#gsrs-fetch-keywords-container');
const fetchKeywordsCheckbox = elements.fetchDescriptionKeywordsCheckbox;
if (fetchDescCheckbox && fetchKeywordsContainer && fetchKeywordsCheckbox) {
const toggleKeywordsOption = () => { fetchKeywordsContainer.style.display = fetchDescCheckbox.checked ? 'block' : 'none'; if (!fetchDescCheckbox.checked) { fetchKeywordsCheckbox.checked = false; } };
fetchDescCheckbox.addEventListener('change', toggleKeywordsOption);
toggleKeywordsOption();
}
const detailsElements = settingsPanel.querySelectorAll('details');
detailsElements.forEach(details => {
details.addEventListener('toggle', (event) => {
if (event.target.open) {
detailsElements.forEach(otherDetails => {
if (otherDetails !== event.target && otherDetails.open) {
otherDetails.open = false;
}
});
}
});
});
}
},
attachActionLinkEvents: function() {
const actionButtonBar = document.getElementById('gsrs-action-button-bar'); if (!actionButtonBar) return;
actionButtonBar.querySelectorAll('.gsrs-action-format-link').forEach(link => {
link.addEventListener('click', (event) => {
if (link.classList.contains('gsrs-action-disabled')) return;
const action = event.target.dataset.action; const format = event.target.dataset.format;
if (action === 'copy') {
if (format === 'json') handleCopyResults();
else if (format === 'urls') handleCopyUrls();
else if (format === 'md') handleCopyOrDownloadMarkdown('copy');
} else if (action === 'download') {
if (format === 'json') handleDownloadResults('json');
else if (format === 'csv') handleDownloadResults('csv');
else if (format === 'urls') handleDownloadUrls();
else if (format === 'md') handleCopyOrDownloadMarkdown('download');
}
});
});
},
createContextMenu: function() {
const contextMenuElement = document.createElement('div'); contextMenuElement.id = 'gsrs-context-menu';
if (GSRS_App.currentSettings.darkMode) { contextMenuElement.classList.add('gsrs-context-menu-dark'); }
contextMenuElement.innerHTML = ` <div class="gsrs-context-menu-item" data-action="copy-json">Copy Item JSON</div> <div class="gsrs-context-menu-item" data-action="copy-title">Copy Title</div> <div class="gsrs-context-menu-item" data-action="copy-url">Copy URL</div> <div class="gsrs-context-menu-item" data-action="copy-description">Copy Description</div> <div class="gsrs-context-menu-separator"></div> <div class="gsrs-context-menu-item" data-action="open-url">Open URL in New Tab</div> <div class="gsrs-context-menu-item" data-action="highlight-on-page">Highlight Item on Page</div> `;
contextMenuElement.querySelectorAll('.gsrs-context-menu-item').forEach(item => { if (item.dataset.action) { item.addEventListener('click', () => handleContextMenuAction(item.dataset.action)); } });
document.addEventListener('click', (event) => { if (contextMenuElement && contextMenuElement.style.display === 'block') { if (!contextMenuElement.contains(event.target)) { contextMenuElement.style.display = 'none'; GSRS_App.state.currentContextMenuItemData = null; } } });
contextMenuElement.addEventListener('contextmenu', e => e.preventDefault());
return contextMenuElement;
},
toggleSettingsView: function(show) {
const uiContainer = GSRS_App.uiElements.uiContainer; const settingsPanel = GSRS_App.uiElements.settingsPanel; if (!uiContainer || !settingsPanel) return;
const shouldShow = typeof show === 'boolean' ? show : !uiContainer.classList.contains('gsrs-settings-view-active');
GSRS_App.currentSettings.uiSettingsVisible = shouldShow;
uiContainer.classList.toggle('gsrs-settings-view-active', shouldShow);
const titleBarTextEl = document.getElementById('gsrs-title-bar-text');
if (titleBarTextEl) { titleBarTextEl.textContent = GSRS_App.state.originalTitleBarText; }
if (shouldShow) {
const allDetails = settingsPanel.querySelectorAll('details');
let firstOpenFound = false;
allDetails.forEach(details => { if (details.hasAttribute('open')) { if (firstOpenFound) { details.removeAttribute('open'); } else { firstOpenFound = true; } } });
if (!firstOpenFound && allDetails.length > 0 && !Array.from(allDetails).some(d => d.open)) { allDetails[0].open = true;}
}
GSRS_App.settingsManager.saveUIPrefs();
},
toggleDarkMode: function(enable) {
if (typeof enable !== 'boolean') { enable = !GSRS_App.currentSettings.darkMode; }
GSRS_App.currentSettings.darkMode = enable;
if (GSRS_App.uiElements.uiContainer) { GSRS_App.uiElements.uiContainer.classList.toggle('gsrs-dark-theme', GSRS_App.currentSettings.darkMode); }
if (GSRS_App.uiElements.contextMenuElement) { GSRS_App.uiElements.contextMenuElement.classList.toggle('gsrs-context-menu-dark', GSRS_App.currentSettings.darkMode); }
GM_setValue('gsrs_darkMode', GSRS_App.currentSettings.darkMode);
if (GSRS_App.uiElements.settingsDOMElements.darkModeCheckbox) { GSRS_App.uiElements.settingsDOMElements.darkModeCheckbox.checked = GSRS_App.currentSettings.darkMode; }
},
showUIMessage: function(message, type = 'success', duration = 3000, details = null) {
const uiMessageDiv = GSRS_App.uiElements.uiMessageDiv; if (!uiMessageDiv) return;
uiMessageDiv.textContent = message; uiMessageDiv.className = ''; uiMessageDiv.classList.add('gsrs-ui-message'); uiMessageDiv.classList.add(`gsrs-${type}`);
uiMessageDiv.style.display = 'block';
setTimeout(() => { if (uiMessageDiv) uiMessageDiv.style.display = 'none'; }, duration);
if (details && (type === 'error' || GSRS_App.currentSettings.debugMode)) { console[type === 'error' ? 'error' : 'log'](`GSRS UI Message (${type}): ${message}`, details); }
},
makeDraggable: function(element, handle) { makeDraggable(element, handle); },
toggleViewMode: function(mode) { toggleViewMode(mode); },
updateObserverStatus: function(isActive) { // Simplified: always inactive for MutationObserver
const observerStatusSpan = GSRS_App.uiElements.observerStatusSpan;
if (observerStatusSpan) {
observerStatusSpan.innerHTML = '○'; // Circle indicating general inactivity for dynamic loading
observerStatusSpan.classList.remove('gsrs-obs-active');
observerStatusSpan.classList.add('gsrs-obs-inactive');
observerStatusSpan.title = 'Dynamic loading detection inactive';
}
},
updateResultsDisplay: function() { updateResultsDisplay(); },
populateResultsList: function() { populateResultsList(); },
addStyles: function() { GM_addStyle(STYLES); }
};
// STYLES (Reformatted for readability)
const STYLES = `
/* --- GSRS UI Container & Wrapper --- */
#gsrs-ui-container {
position: fixed;
width: ${GSRS_App.DEFAULT_SETTINGS.uiPanelWidth}; /* Injected from default settings */
min-height: 480px;
background-color: #f9f9f9;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 99999; /* High z-index to stay on top */
font-family: Arial, sans-serif;
font-size: 14px;
color: #333;
display: flex;
flex-direction: column;
transition: width 0.2s ease-out, height 0.2s ease-out, top 0.2s ease-out, left 0.2s ease-out, right 0.2s ease-out;
}
#gsrs-ui-container.gsrs-maximized-panel {
min-height: unset; /* Allow full screen height */
}
#gsrs-ui-container.gsrs-minimized #gsrs-content-wrapper {
display: none !important;
}
#gsrs-ui-container.gsrs-minimized {
min-height: unset !important;
height: auto !important; /* Collapse to title bar height */
}
#gsrs-content-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1; /* Take available vertical space */
overflow: hidden; /* Prevent content spill */
min-height: 0; /* For flex child proper shrinking */
box-sizing: border-box;
padding: 10px;
}
/* --- Title Bar --- */
#gsrs-title-bar {
padding: 8px 10px;
background-color: #eee;
border-bottom: 1px solid #ccc;
cursor: move;
user-select: none;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0; /* Don't shrink title bar */
}
#gsrs-settings-toggle-titlebar {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 0 8px 0 0;
color: #333;
margin-right: auto; /* Pushes other controls to the right */
flex-shrink: 0;
}
#gsrs-title-bar-text {
text-align: center;
flex-grow: 1;
font-weight: bold;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#gsrs-title-bar-controls {
display: flex;
align-items: center;
flex-shrink: 0;
}
#gsrs-ui-container.gsrs-minimized #gsrs-settings-toggle-titlebar {
margin-right: 5px !important; /* Adjust spacing when minimized */
}
#gsrs-ui-container.gsrs-minimized #gsrs-title-bar-text {
display: block !important; /* Ensure it's visible */
flex-grow: 1 !important;
text-align: center !important;
margin-left: 5px;
margin-right: 5px;
}
#gsrs-observer-status {
font-size: 18px;
margin-right: 10px;
}
#gsrs-observer-status.gsrs-obs-active { color: green; } /* Kept for potential future use */
#gsrs-observer-status.gsrs-obs-inactive { color: #FF8C00; } /* Orange for inactive/URL mode */
#gsrs-maximize-btn,
#gsrs-minimize-btn {
cursor: pointer;
background: none;
border: none;
font-size: 16px;
padding: 0 5px;
color: #333;
margin-left: 5px;
}
/* --- Main Content Areas (Actions, Filter, Results Display etc.) --- */
.gsrs-main-actions,
.gsrs-filter-container,
#gsrs-results-count-wrapper,
.gsrs-copy-download-options,
.gsrs-action-button-bar,
#gsrs-ui-message,
#gsrs-results-view-area {
padding-left: 0; /* Reset any inherited padding */
padding-right: 0;
}
.gsrs-main-actions {
margin-bottom: 10px;
display: flex;
gap: 5px;
}
.gsrs-filter-container {
margin-bottom: 5px;
display: flex;
flex-shrink: 0; /* Prevent filter area from shrinking too much */
}
#gsrs-results-count-wrapper {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.gsrs-copy-download-options {
margin-bottom: 5px;
padding-top: 5px;
padding-bottom: 5px;
background-color: #f0f0f0;
border-radius:3px;
flex-shrink: 0;
text-align: center;
font-size: 12px;
}
.gsrs-action-button-bar {
margin-top: 8px;
display: flex;
gap: 15px; /* Space between copy/download sets */
align-items: center;
justify-content: center;
flex-shrink: 0;
flex-wrap: wrap; /* Allow wrapping if panel is too narrow */
font-size: 13px;
}
#gsrs-ui-message {
margin-top: 5px;
font-size: 12px;
padding-top: 5px;
padding-bottom: 5px;
border-radius: 3px;
text-align: center;
display: none; /* Shown by JS */
flex-shrink: 0;
}
#gsrs-results-view-area {
display: flex;
flex-direction: column; /* Stack list and preview vertically */
flex-grow: 1;
min-height: 0; /* For flex child proper shrinking */
overflow: hidden; /* Child elements will handle their own scroll */
margin-bottom: 5px;
}
/* --- Buttons & Inputs --- */
.gsrs-button {
padding: 8px 12px;
margin-right: 5px; /* Default spacing, overridden where needed */
margin-bottom: 5px; /* Default spacing */
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 13px;
text-align: center;
display: inline-flex; /* For icon alignment */
align-items: center;
justify-content: center;
line-height: 1; /* Consistent line height */
}
.gsrs-button:hover { opacity: 0.9; }
#gsrs-start-btn {
background-color: #4CAF50; /* Green */
flex-grow: 1; /* Take available space in main-actions */
margin-right: 0;
margin-bottom: 0;
}
#gsrs-clear-btn {
background-color: #f44336; /* Red */
margin-right: 0;
margin-bottom: 0;
flex-shrink: 0;
}
#gsrs-filter-input {
flex-grow: 1;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
margin-right: 5px; /* Space before clear button */
height: 34px; /* Match button height */
box-sizing: border-box;
}
#gsrs-filter-input.gsrs-filter-active {
border-color: #FF9800; /* Orange highlight */
box-shadow: 0 0 3px #FF980080;
}
#gsrs-clear-filter-btn {
padding: 0 10px; /* Horizontal padding only */
font-size: 16px;
background-color: #9E9E9E; /* Grey */
color: white;
min-width: auto; /* Allow natural width */
flex-shrink: 0;
height: 34px; /* Match input height */
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
border: none;
border-radius: 3px;
cursor: pointer;
}
.gsrs-copy-download-options label {
margin-right: 15px;
cursor: pointer;
}
.gsrs-copy-download-options input[type="radio"] {
margin-right: 4px;
vertical-align: middle;
cursor: pointer;
}
/* --- Results Display (JSON, List, Preview) --- */
#gsrs-results-area { /* JSON View Textarea */
width: 100%;
box-sizing: border-box;
flex-grow: 1;
border: 1px solid #ddd;
border-radius: 3px;
padding: 5px;
font-family: monospace;
font-size: 12px;
resize: none; /* Or 'vertical' if preferred */
}
#gsrs-results-count {
font-size: 12px;
color: #555;
}
.gsrs-view-toggle-buttons button {
font-size: 11px;
padding: 3px 6px;
background-color: #e0e0e0;
color: #333;
border: 1px solid #ccc;
margin-left: 5px;
}
.gsrs-view-toggle-buttons button.active {
background-color: #c0c0c0;
font-weight: bold;
}
#gsrs-results-list-container {
display: none; /* Toggled by JS */
flex-direction: column;
width: 100%;
flex-grow: 3; /* Takes more space than preview */
flex-shrink: 1;
flex-basis: 150px; /* Initial basis */
min-height: 120px; /* Minimum scrollable area */
max-height: 100%; /* Don't exceed parent */
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 3px;
padding: 5px;
margin-bottom:5px; /* Space between list and preview */
background-color: #fff;
box-sizing: border-box;
}
#gsrs-results-list-container .gsrs-list-item {
padding: 3px 5px;
cursor: pointer;
border-bottom: 1px dotted #eee;
font-size: 12px;
line-height: 1.4;
overflow-wrap: break-word;
user-select: none;
}
#gsrs-results-list-container .gsrs-list-item:last-child {
border-bottom: none;
}
#gsrs-results-list-container .gsrs-list-item:hover {
background-color: #f0f0f0;
}
#gsrs-results-list-container .gsrs-list-item.selected {
background-color: #d0e0ff; /* Light blue selection */
font-weight: bold;
}
#gsrs-result-preview-area {
display: none; /* Toggled by JS */
width: 100%;
flex-grow: 1; /* Takes less space than list */
flex-shrink: 1;
flex-basis: 80px; /* Initial basis */
min-height: 80px; /* Minimum scrollable area */
max-height: 30%; /* Limit height relative to parent */
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 3px;
padding: 5px;
background-color: #fff;
font-size: 12px;
box-sizing: border-box;
}
#gsrs-result-preview-area p {
margin: 0 0 5px 0;
word-break: break-all;
}
#gsrs-result-preview-area strong { font-weight: bold; }
#gsrs-result-preview-area a { color: #1a0dab; text-decoration: none; }
#gsrs-result-preview-area a:hover { text-decoration: underline; }
/* --- UI Messages --- */
#gsrs-ui-message.gsrs-success {
background-color: #d4edda; /* Bootstrap success green */
color: #155724;
border: 1px solid #c3e6cb;
}
#gsrs-ui-message.gsrs-error {
background-color: #f8d7da; /* Bootstrap error red */
color: #721c24;
border: 1px solid #f5c6cb;
}
/* --- Settings Panel --- */
#gsrs-settings-panel {
display: none; /* Shown when .gsrs-settings-view-active on container */
flex-direction: column;
overflow-y: auto;
padding: 10px;
background-color: #fff;
box-sizing: border-box;
}
#gsrs-ui-container.gsrs-settings-view-active #gsrs-content-wrapper > *:not(#gsrs-settings-panel) {
display: none !important;
}
#gsrs-ui-container.gsrs-settings-view-active #gsrs-settings-panel {
display: flex !important;
flex-grow: 1;
min-height: 0;
}
#gsrs-settings-panel details {
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
}
#gsrs-settings-panel summary {
padding: 8px;
background-color: #f7f7f7;
cursor: pointer;
font-weight: bold;
list-style-position: inside;
border-bottom: 1px solid #eee;
}
#gsrs-settings-panel details[open] summary {
border-bottom: 1px solid #eee;
}
#gsrs-settings-panel details > div {
padding: 10px;
}
.gsrs-export-options-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 5px;
}
.gsrs-setting-item { margin-bottom: 8px; }
.gsrs-setting-item.gsrs-setting-group-divider {
margin-top: 15px;
margin-bottom: 10px;
border-top: 1px dashed #ccc;
padding-top: 10px;
}
.gsrs-setting-item label { /* General labels in settings */
display: block;
margin-bottom: 4px;
font-weight: bold;
font-size: 12px;
}
.gsrs-setting-input-group {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.gsrs-setting-item input[type="text"] {
flex-grow: 1;
min-width: 150px;
padding: 4px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
margin-right: 5px;
}
.gsrs-setting-item input[type="checkbox"] {
margin-right: 5px;
vertical-align: middle;
}
#gsrs-btn-save-settings { background-color: #007bff; /* Blue */ }
#gsrs-btn-reset-settings { background-color: #dc3545; /* Red */ }
.gsrs-settings-warning {
font-size: 11px;
color: #777;
margin-top: 10px;
}
.gsrs-test-selector-btn {
background-color: #607D8B !important; /* Blue Grey */
font-size: 11px !important;
padding: 4px 8px !important;
margin-left: 5px;
flex-shrink: 0;
color: white !important;
}
.gsrs-test-result-span {
font-size: 11px;
margin-left: 8px;
color: #3F51B5; /* Indigo */
white-space: nowrap;
}
.gsrs-selector-test-preview {
max-height: 150px;
overflow-y: auto;
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 5px;
margin-top: 5px;
font-family: monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
display: none;
}
.gsrs-selector-test-preview strong {
font-weight: bold;
display: block;
margin-bottom: 3px;
font-size: 10px;
color: #555;
}
.gsrs-dark-theme .gsrs-selector-test-preview div {
background-color: #2c2c2c !important;
color: #e0e0e0 !important;
border: 1px solid #4a4a4a !important;
}
/* --- Action Links (Copy/Download) --- */
.gsrs-action-set {
display: flex;
align-items: center;
gap: 4px;
}
.gsrs-action-icon {
font-size: 1.1em;
margin-right: 2px;
}
.gsrs-action-format-link {
color: #007bff;
text-decoration: none;
cursor: pointer;
}
.gsrs-action-format-link:hover {
text-decoration: underline;
color: #0056b3;
}
.gsrs-action-format-link.gsrs-action-disabled {
color: #bbbbbb !important;
cursor: default;
pointer-events: none;
text-decoration: none !important;
}
/* --- Highlighting Styles --- */
.gsrs-highlighted {
outline: 2px dashed #FF9800 !important; /* Orange */
box-shadow: 0 0 10px #FF980080 !important;
transition: outline 0.5s ease-out, box-shadow 0.5s ease-out;
}
.gsrs-selector-test-highlight {
outline: 2px dashed #4CAF50 !important; /* Green */
background-color: rgba(76, 175, 80, 0.1) !important;
box-shadow: 0 0 8px rgba(76, 175, 80, 0.5) !important;
transition: outline 0.3s ease-out, background-color 0.3s ease-out, box-shadow 0.3s ease-out;
}
.gsrs-context-highlight {
outline: 3px solid #2196F3 !important; /* Blue */
background-color: rgba(33, 150, 243, 0.15) !important;
box-shadow: 0 0 10px rgba(33, 150, 243, 0.6) !important;
transition: outline 0.4s ease-in-out, background-color 0.4s ease-in-out, box-shadow 0.4s ease-in-out;
}
.gsrs-list-item-page-highlight {
outline: 3px solid #00BCD4 !important; /* Cyan */
background-color: rgba(0, 188, 212, 0.15) !important;
box-shadow: 0 0 10px rgba(0, 188, 212, 0.6) !important;
transition: outline 0.4s ease-in-out, background-color 0.4s ease-in-out, box-shadow 0.4s ease-in-out;
}
/* --- Context Menu --- */
#gsrs-context-menu {
position: fixed;
display: none;
background-color: #ffffff;
border: 1px solid #cccccc;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-radius: 4px;
padding: 5px 0;
z-index: 100000;
font-size: 13px;
min-width: 180px;
color: #333333;
}
.gsrs-context-menu-item {
padding: 7px 15px;
cursor: pointer;
user-select: none;
color: #333333;
}
.gsrs-context-menu-item:hover {
background-color: #f0f0f0;
color: #111111;
}
.gsrs-context-menu-item.gsrs-cm-disabled {
color: #aaaaaa !important;
cursor: default !important;
background-color: transparent !important;
}
.gsrs-context-menu-separator {
height: 1px;
background-color: #e0e0e0;
margin: 4px 0;
}
/* --- Dark Theme --- */
#gsrs-ui-container.gsrs-dark-theme {
background-color: #2e2e2e;
color: #e0e0e0;
border: 1px solid #4a4a4a;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-title-bar {
background-color: #202124;
border-bottom: 1px solid #4a4a4a;
color: #e0e0e0;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-settings-toggle-titlebar,
#gsrs-ui-container.gsrs-dark-theme #gsrs-maximize-btn,
#gsrs-ui-container.gsrs-dark-theme #gsrs-minimize-btn,
#gsrs-ui-container.gsrs-dark-theme #gsrs-observer-status {
color: #e0e0e0;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-observer-status.gsrs-obs-active { color: #6fbf73; /* Lighter green for dark bg */ }
#gsrs-ui-container.gsrs-dark-theme #gsrs-observer-status.gsrs-obs-inactive { color: #FFB74D; /* Lighter Orange */ }
#gsrs-ui-container.gsrs-dark-theme input[type="text"],
#gsrs-ui-container.gsrs-dark-theme textarea {
background-color: #3c4043;
color: #e0e0e0;
border: 1px solid #5f6368;
}
#gsrs-ui-container.gsrs-dark-theme input[type="text"]::placeholder,
#gsrs-ui-container.gsrs-dark-theme textarea::placeholder {
color: #9e9e9e;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-filter-input.gsrs-filter-active {
border-color: #fdd835; /* Yellow for dark theme */
box-shadow: 0 0 3px #fdd83580;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-start-btn { background-color: #388e3c; /* Darker Green */ }
#gsrs-ui-container.gsrs-dark-theme #gsrs-clear-btn { background-color: #d32f2f; /* Darker Red */ }
#gsrs-ui-container.gsrs-dark-theme #gsrs-clear-filter-btn { background-color: #4a4a4a !important; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-test-selector-btn { background-color: #455A64 !important; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-copy-download-options {
background-color: #3c4043;
border: 1px solid #4a4a4a;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-count { color: #bdbdbd; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-view-toggle-buttons button {
background-color: #424242;
color: #e0e0e0;
border: 1px solid #5f6368;
}
#gsrs-ui-container.gsrs-dark-theme .gsrs-view-toggle-buttons button.active { background-color: #535353; }
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-area,
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-list-container,
#gsrs-ui-container.gsrs-dark-theme #gsrs-result-preview-area {
background-color: #202124; /* Very dark grey, like Google's dark theme */
border: 1px solid #4a4a4a;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-settings-panel { background-color: #2e2e2e; }
#gsrs-ui-container.gsrs-dark-theme #gsrs-settings-panel details { border: 1px solid #4a4a4a; }
#gsrs-ui-container.gsrs-dark-theme #gsrs-settings-panel summary {
background-color: #3c4043;
border-bottom: 1px solid #4a4a4a;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-settings-panel details[open] summary { border-bottom: 1px solid #4a4a4a; }
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-list-container .gsrs-list-item {
border-bottom: 1px dotted #4a4a4a;
color: #e0e0e0;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-list-container .gsrs-list-item:hover { background-color: #3c4043; }
#gsrs-ui-container.gsrs-dark-theme #gsrs-results-list-container .gsrs-list-item.selected {
background-color: #334e7c; /* Darker blue for selection */
font-weight: bold;
}
#gsrs-ui-container.gsrs-dark-theme #gsrs-result-preview-area a { color: #8ab4f8; /* Google's dark theme link color */ }
#gsrs-ui-container.gsrs-dark-theme .gsrs-action-format-link { color: #8ab4f8; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-action-format-link:hover { color: #aecbfa; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-action-format-link.gsrs-action-disabled { color: #777777 !important; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-setting-item label { color: #e0e0e0; }
#gsrs-ui-container.gsrs-dark-theme .gsrs-setting-input-group input[type="checkbox"] + label {
color: #e0e0e0 !important;
font-weight: normal !important;
}
#gsrs-ui-container.gsrs-dark-theme .gsrs-settings-warning { color: #bdbdbd; }
.gsrs-dark-theme .gsrs-selector-test-preview {
background-color: #3c4043;
border: 1px solid #5f6368;
}
.gsrs-dark-theme .gsrs-selector-test-preview div {
background-color: #2c2c2c !important;
color: #e0e0e0 !important;
border: 1px solid #4a4a4a !important;
}
.gsrs-dark-theme .gsrs-selector-test-preview strong { color: #bdbdbd; }
.gsrs-dark-theme .gsrs-test-result-span { color: #81d4fa; /* Light blue */ }
#gsrs-ui-container.gsrs-dark-theme .gsrs-ui-message.gsrs-success {
background-color: #2E7D32; color: #C8E6C9; border: 1px solid #388E3C;
}
#gsrs-ui-container.gsrs-dark-theme .gsrs-ui-message.gsrs-error {
background-color: #C62828; color: #FFCDD2; border: 1px solid #D32F2F;
}
/* Dark theme scrollbar */
#gsrs-ui-container.gsrs-dark-theme ::-webkit-scrollbar { width: 10px; height: 10px; }
#gsrs-ui-container.gsrs-dark-theme ::-webkit-scrollbar-track { background: #2e2e2e; }
#gsrs-ui-container.gsrs-dark-theme ::-webkit-scrollbar-thumb { background: #555; border-radius: 5px; border: 2px solid #2e2e2e; }
#gsrs-ui-container.gsrs-dark-theme ::-webkit-scrollbar-thumb:hover { background: #666; }
#gsrs-ui-container.gsrs-dark-theme select {
background-color: #3c4043;
color: #e0e0e0;
border: 1px solid #5f6368;
}
/* Dark theme context menu */
#gsrs-context-menu.gsrs-context-menu-dark {
background-color: #383838;
border-color: #585858;
color: #e0e0e0;
}
#gsrs-context-menu.gsrs-context-menu-dark .gsrs-context-menu-item { color: #e0e0e0; }
#gsrs-context-menu.gsrs-context-menu-dark .gsrs-context-menu-item:hover { background-color: #4f4f4f; color: #ffffff; }
#gsrs-context-menu.gsrs-context-menu-dark .gsrs-context-menu-item.gsrs-cm-disabled { color: #777777 !important; }
#gsrs-context-menu.gsrs-context-menu-dark .gsrs-context-menu-separator { background-color: #5f6368; }
`;
// Punycode.js
const punycode = (() => {
const maxInt = 2147483647; const base = 36; const tMin = 1; const tMax = 26; const skew = 38; const damp = 700; const initialBias = 72; const initialN = 128; const delimiter = '-'; const errors = { 'overflow': 'Overflow: input needs wider integers to process', 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', 'invalid-input': 'Invalid input' }; const baseMinusTMin = base - tMin; function error(type) { throw new RangeError(errors[type]); } function basicToDigit(codePoint) { if (codePoint - 0x30 < 0x0A) { return codePoint - 0x16; } if (codePoint - 0x41 < 0x1A) { return codePoint - 0x41; } if (codePoint - 0x61 < 0x1A) { return codePoint - 0x61; } return base; } function adapt(delta, numPoints, firstTime) { let k = 0; delta = firstTime ? Math.floor(delta / damp) : delta >> 1; delta += Math.floor(delta / numPoints); for (; delta > baseMinusTMin * tMax >> 1; k += base) { delta = Math.floor(delta / baseMinusTMin); } return Math.floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); } function decode(input) { const output = []; const inputLength = input.length; let i = 0; let n = initialN; let bias = initialBias; let basic = input.lastIndexOf(delimiter); if (basic < 0) { basic = 0; } for (let j = 0; j < basic; ++j) { if (input.charCodeAt(j) >= 0x80) { error('not-basic'); } output.push(input.charCodeAt(j)); } for (let index = basic > 0 ? basic + 1 : 0; index < inputLength;) { let oldi = i; let w = 1; for (let k = base; ; k += base) { if (index >= inputLength) { error('invalid-input'); } const digit = basicToDigit(input.charCodeAt(index++)); if (digit >= base || digit > Math.floor((maxInt - i) / w)) { error('overflow'); } i += digit * w; const t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); if (digit < t) { break; } const baseMinusT = base - t; if (w > Math.floor(maxInt / baseMinusT)) { error('overflow'); } w *= baseMinusT; } const out = output.length + 1; bias = adapt(i - oldi, out, oldi == 0); if (Math.floor(i / out) > maxInt - n) { error('overflow'); } n += Math.floor(i / out); i %= out; output.splice(i++, 0, n); } return String.fromCodePoint(...output); } return { decode: decode, };
})();
// Date Parsing Functions
function getPageLanguageSimple() { const htmlLang = document.documentElement.lang; if (htmlLang) { const langPart = htmlLang.split('-')[0].toLowerCase(); const regionPart = htmlLang.split('-')[1]?.toLowerCase(); if (langPart === "zh") { if (regionPart === "tw" || regionPart === "hk" || htmlLang.toLowerCase().includes("hant")) return "zh-tw"; return "zh-tw"; } if (langPart === "ja") return "ja"; if (langPart === "en") return "en"; } return "unknown"; }
const englishMonths = { jan: 0, january: 0, feb: 1, february: 1, mar: 2, march: 2, apr: 3, april: 3, may: 4, jun: 5, june: 5, jul: 6, july: 6, aug: 7, august: 7, sep: 8, sept: 8, september: 8, oct: 9, october: 9, nov: 10, november: 10, dec: 11, december: 11 };
function parseDateStringPureJS(textDate, refDate = new Date()) { if (!textDate || typeof textDate !== 'string' || textDate.trim() === '') { return null; } const originalTextDate = textDate.trim(); let textForMatching = originalTextDate.toLowerCase(); const debug = GSRS_App.currentSettings && GSRS_App.currentSettings.debugMode; const lang = getPageLanguageSimple(); let year, month, day, hour, minute, second; const tempDate = new Date(refDate.getTime()); if (debug) console.log(`GSRS (PureJSParse Attempt): "${originalTextDate}", Lang: ${lang}, RefDate: ${refDate.toISOString()}`); if (lang === 'en' || lang === 'unknown') { let match = textForMatching.match(/^(\d+)\s+(hour|minute|second|day|week|month|year)s?\s+ago$/); if (match) { const value = parseInt(match[1]); const unit = match[2]; if (unit === 'hour') tempDate.setHours(tempDate.getHours() - value); else if (unit === 'minute') tempDate.setMinutes(tempDate.getMinutes() - value); else if (unit === 'second') tempDate.setSeconds(tempDate.getSeconds() - value); else if (unit === 'day') tempDate.setDate(tempDate.getDate() - value); else if (unit === 'week') tempDate.setDate(tempDate.getDate() - (value * 7)); else if (unit === 'month') tempDate.setMonth(tempDate.getMonth() - value); else if (unit === 'year') tempDate.setFullYear(tempDate.getFullYear() - value); if (debug) console.log(`GSRS (PureJSParse EN Rel): "${originalTextDate}" -> ${tempDate.toISOString()}`); return tempDate; } if (textForMatching === "yesterday") { tempDate.setDate(tempDate.getDate() - 1); tempDate.setHours(0, 0, 0, 0); if (debug) console.log(`GSRS (PureJSParse EN Rel): "yesterday" -> ${tempDate.toISOString()}`); return tempDate; } if (textForMatching === "just now" || textForMatching === "now") { if (debug) console.log(`GSRS (PureJSParse EN Rel): "just now/now" -> ${refDate.toISOString()}`); return new Date(refDate.getTime()); } } if (lang === 'ja') { let match = originalTextDate.match(/^(\d+)\s*時間前$/); if (match) { tempDate.setHours(tempDate.getHours() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse JA Rel): hour -> ${tempDate.toISOString()}`); return tempDate; } match = originalTextDate.match(/^(\d+)\s*分前$/); if (match) { tempDate.setMinutes(tempDate.getMinutes() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse JA Rel): minute -> ${tempDate.toISOString()}`); return tempDate; } match = originalTextDate.match(/^(\d+)\s*日前$/); if (match) { tempDate.setDate(tempDate.getDate() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse JA Rel): day -> ${tempDate.toISOString()}`); return tempDate; } if (originalTextDate === "昨日") { tempDate.setDate(tempDate.getDate() - 1); tempDate.setHours(0, 0, 0, 0); if (debug) console.log(`GSRS (PureJSParse JA Rel): "昨日" -> ${tempDate.toISOString()}`); return tempDate; } if (originalTextDate === "たった今" || originalTextDate === "今さっき") { if (debug) console.log(`GSRS (PureJSParse JA Rel): "たった今/今さっき" -> ${refDate.toISOString()}`); return new Date(refDate.getTime()); } } if (lang === 'zh-tw') { let match = originalTextDate.match(/^(\d+)\s*小時前$/); if (match) { tempDate.setHours(tempDate.getHours() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse ZH-TW Rel): hour -> ${tempDate.toISOString()}`); return tempDate; } match = originalTextDate.match(/^(\d+)\s*分鐘前$/); if (match) { tempDate.setMinutes(tempDate.getMinutes() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse ZH-TW Rel): minute -> ${tempDate.toISOString()}`); return tempDate; } match = originalTextDate.match(/^(\d+)\s*天前$/); if (match) { tempDate.setDate(tempDate.getDate() - parseInt(match[1])); if (debug) console.log(`GSRS (PureJSParse ZH-TW Rel): day -> ${tempDate.toISOString()}`); return tempDate; } if (originalTextDate === "昨天") { tempDate.setDate(tempDate.getDate() - 1); tempDate.setHours(0, 0, 0, 0); if (debug) console.log(`GSRS (PureJSParse ZH-TW Rel): "昨天" -> ${tempDate.toISOString()}`); return tempDate; } if (originalTextDate === "剛剛" || originalTextDate === "剛") { if (debug) console.log(`GSRS (PureJSParse ZH-TW Rel): "剛剛/剛" -> ${refDate.toISOString()}`); return new Date(refDate.getTime());} } let parsed, isValid, hasTimePartInString; let engAbsMatch = originalTextDate.match(/^([a-z.]{3,9})\s+(\d{1,2})(?:st|nd|rd|th)?(?:,\s*|\s+)(\d{4})(?:\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(am|pm)?)?$/i); if (engAbsMatch) { const monthName = engAbsMatch[1].toLowerCase().replace('.', ''); month = englishMonths[monthName]; day = parseInt(engAbsMatch[2]); year = parseInt(engAbsMatch[3]); hasTimePartInString = false; if (typeof month === 'number' && month >= 0 && month <= 11) { hour = 0; minute = 0; second = 0; if (engAbsMatch[4] && engAbsMatch[5]) { hasTimePartInString = true; hour = parseInt(engAbsMatch[4]); minute = parseInt(engAbsMatch[5]); second = engAbsMatch[6] ? parseInt(engAbsMatch[6]) : 0; const ampm = engAbsMatch[7] ? engAbsMatch[7].toLowerCase() : null; if (ampm === 'pm' && hour < 12) hour += 12; if (ampm === 'am' && hour === 12) hour = 0; } parsed = hasTimePartInString ? new Date(year, month, day, hour, minute, second) : new Date(Date.UTC(year, month, day)); isValid = hasTimePartInString ? (parsed.getFullYear() === year && parsed.getMonth() === month && parsed.getDate() === day && parsed.getHours() === hour && parsed.getMinutes() === minute) : (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month && parsed.getUTCDate() === day); if (isValid) { if (debug) console.log(`GSRS (PureJSParse EN Abs MonthDDYYYY): "${originalTextDate}" -> ${parsed.toISOString()} (UTC: ${!hasTimePartInString})`); return parsed; } else if (debug) { console.log(`GSRS (PureJSParse EN Abs MonthDDYYYY): Invalid date components for "${originalTextDate}" -> Y:${year},M:${month},D:${day}, H:${hour},m:${minute} (UTC: ${!hasTimePartInString})`); } } } let cjkDateParts = originalTextDate.match(/(\d{4})年\s*(\d{1,2})月\s*(\d{1,2})日?(?:\s*(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/); if (cjkDateParts) { year = parseInt(cjkDateParts[1]); month = parseInt(cjkDateParts[2]) - 1; day = parseInt(cjkDateParts[3]); hasTimePartInString = !!(cjkDateParts[4] && cjkDateParts[5]); hour = hasTimePartInString ? parseInt(cjkDateParts[4]) : 0; minute = hasTimePartInString ? parseInt(cjkDateParts[5]) : 0; second = (hasTimePartInString && cjkDateParts[6]) ? parseInt(cjkDateParts[6]) : 0; parsed = hasTimePartInString ? new Date(year, month, day, hour, minute, second) : new Date(Date.UTC(year, month, day)); isValid = hasTimePartInString ? (parsed.getFullYear() === year && parsed.getMonth() === month && parsed.getDate() === day) : (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month && parsed.getUTCDate() === day); if (isValid) { if (debug) console.log(`GSRS (PureJSParse CJK Abs YMD): "${originalTextDate}" -> ${parsed.toISOString()} (UTC: ${!hasTimePartInString})`); return parsed; } } cjkDateParts = !parsed ? originalTextDate.match(/(\d{1,2})月\s*(\d{1,2})日?/) : null; if (cjkDateParts) { month = parseInt(cjkDateParts[1]) - 1; day = parseInt(cjkDateParts[2]); hasTimePartInString = false; year = refDate.getFullYear(); const tempProspectiveDateThisYear = new Date(Date.UTC(year, month, day)); if (tempProspectiveDateThisYear.getTime() > refDate.getTime() && (month > refDate.getUTCMonth() || (month === refDate.getUTCMonth() && day > refDate.getUTCDate()))) { year--; } parsed = new Date(Date.UTC(year, month, day)); isValid = (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month && parsed.getUTCDate() === day); if (isValid) { if (debug) console.log(`GSRS (PureJSParse CJK Abs MD): "${originalTextDate}" -> ${parsed.toISOString()} (UTC: true)`); return parsed; } } let isoDateParts = !parsed ? originalTextDate.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/) : null; if (isoDateParts) { year = parseInt(isoDateParts[1]); month = parseInt(isoDateParts[2]) - 1; day = parseInt(isoDateParts[3]); hasTimePartInString = !!(isoDateParts[4] && isoDateParts[5]); hour = hasTimePartInString ? parseInt(isoDateParts[4]) : 0; minute = hasTimePartInString ? parseInt(isoDateParts[5]) : 0; second = (hasTimePartInString && isoDateParts[6]) ? parseInt(isoDateParts[6]) : 0; parsed = hasTimePartInString ? new Date(year, month, day, hour, minute, second) : new Date(Date.UTC(year, month, day)); isValid = hasTimePartInString ? (parsed.getFullYear() === year && parsed.getMonth() === month && parsed.getDate() === day) : (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month && parsed.getUTCDate() === day); if (isValid) { if (debug) console.log(`GSRS (PureJSParse ISO-like): "${originalTextDate}" -> ${parsed.toISOString()} (UTC: ${!hasTimePartInString})`); return parsed; } } let mdDateParts = !parsed ? originalTextDate.match(/^(\d{1,2})[./](\d{1,2})[./](\d{4})$/) : null; if (mdDateParts) { hasTimePartInString = false; let mAttempt, dAttempt; if (lang === 'en' || lang === 'unknown') { mAttempt = parseInt(mdDateParts[1]) - 1; dAttempt = parseInt(mdDateParts[2]); } else { dAttempt = parseInt(mdDateParts[1]); mAttempt = parseInt(mdDateParts[2]) - 1; } year = parseInt(mdDateParts[3]); parsed = new Date(Date.UTC(year, mAttempt, dAttempt)); isValid = (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === mAttempt && parsed.getUTCDate() === dAttempt); if (isValid) { if (debug) console.log(`GSRS (PureJSParse M/D/YYYY or D.M.YYYY like): "${originalTextDate}" -> ${parsed.toISOString()} (UTC: true)`); return parsed; } } if (debug) console.log(`GSRS (PureJSParse): Failed to parse "${originalTextDate}" with all PureJS rules.`); return null; }
// --- Utility Functions ---
function isValidSelector(selector) { if (!selector || typeof selector !== 'string' || selector.trim() === '') return false; try { document.querySelector(selector); return true; } catch (e) { return false; } }
function convertToMarkdownList(dataArray) { if (!dataArray || dataArray.length === 0) { return ''; } let mdString = ""; const headersOrder = [ 'position', 'title', 'url', 'siteName', 'breadcrumbs', 'description', 'highlightedSnippets', 'originalDateText', 'parsedDateISO' ]; dataArray.forEach(item => { let itemMd = ""; let hasContent = false; headersOrder.forEach(key => { const settingKey = `exportCsvMd${key.charAt(0).toUpperCase() + key.slice(1)}`; if (item.hasOwnProperty(key) && GSRS_App.currentSettings[settingKey]) { if (!hasContent && item.position) { itemMd += `- **Position:** ${item.position || 'N/A'}\n`; hasContent = true; } else if (key !== 'position') { let value = item[key] === null || typeof item[key] === 'undefined' ? 'N/A' : item[key]; if (key === 'url' && item[key] && item[key] !== 'N/A') { value = `[${item[key].replace(/([\[\]])/g, "\\$1")}](${item[key].replace(/\)/g, "%29")})`; } else if (typeof value === 'string') { value = value.replace(/\n/g, ' '); } let displayName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); if (key === 'highlightedSnippets') displayName = 'Keywords'; if (key === 'originalDateText') displayName = 'Original Date'; if (key === 'parsedDateISO') displayName = 'Parsed Date'; itemMd += ` - **${displayName}:** ${value}\n`; hasContent = true; } } }); if (hasContent) { mdString += itemMd + "\n"; } }); return mdString.trim(); }
function convertToCSV(dataArray) { if (!dataArray || dataArray.length === 0) { return ''; } const escapeCSVField = (field) => { if (field === null || typeof field === 'undefined') { return ''; } const stringField = String(field); if (stringField.includes(',') || stringField.includes('"') || stringField.includes('\n') || stringField.includes('\r')) { return `"${stringField.replace(/"/g, '""')}"`; } return stringField; }; const firstItemKeys = Object.keys(dataArray[0]); const headers = firstItemKeys.filter(key => { const settingKey = `exportCsvMd${key.charAt(0).toUpperCase() + key.slice(1)}`; return GSRS_App.currentSettings[settingKey] === true; }); if (headers.length === 0) return ''; const csvRows = []; csvRows.push(headers.map(header => escapeCSVField(header)).join(',')); for (const item of dataArray) { const row = headers.map(header => { return escapeCSVField(item[header]); }); csvRows.push(row.join(',')); } return csvRows.join('\r\n'); }
function decodeUrlIfEnabled(urlString) { if (!GSRS_App.currentSettings.decodeUrlsToReadable || !urlString || urlString === "Extraction Failed") { return urlString; } let decodedUrl = urlString; try { decodedUrl = decodeURIComponent(urlString.replace(/\+/g, '%20')); } catch (e) { if (GSRS_App.currentSettings.debugMode) console.warn(`GSRS: decodeURIComponent failed for "${urlString}". Using intermediate: "${decodedUrl}". Error: ${e.message}`); } try { const tempUrl = (decodedUrl.startsWith('http://') || decodedUrl.startsWith('https://') || decodedUrl.startsWith('//')) ? decodedUrl : 'http://' + decodedUrl; const urlObj = new URL(tempUrl); let processedHostname = urlObj.hostname; if (processedHostname.includes('xn--')) { const labels = processedHostname.split('.'); const decodedLabels = labels.map(label => { if (label.startsWith('xn--')) { try { return punycode.decode(label.substring(4)); } catch (punyError) { if (GSRS_App.currentSettings.debugMode) console.error(`GSRS: Punycode.js decoding failed for label "${label}" in "${processedHostname}": ${punyError.message}. Original error:`, punyError); return label; } } return label; }); processedHostname = decodedLabels.join('.'); } let path = urlObj.pathname; let search = urlObj.search; let hash = urlObj.hash; try { path = decodeURIComponent(path.replace(/\+/g, '%20')); } catch (e) { /* keep as is */ } try { search = decodeURIComponent(search.replace(/\+/g, '%20')); } catch (e) { /* keep as is */ } try { hash = decodeURIComponent(hash.replace(/\+/g, '%20')); } catch (e) { /* keep as is */ } let finalProtocol = urlObj.protocol; if (urlString.startsWith('//')) { finalProtocol = ''; } let newReconstructedUrl = `${finalProtocol}${finalProtocol ? '//' : ''}${processedHostname}`; if (urlObj.port) newReconstructedUrl += `:${urlObj.port}`; newReconstructedUrl += path + search + hash; decodedUrl = newReconstructedUrl; } catch (e) { if (GSRS_App.currentSettings.debugMode) console.error(`GSRS: Error during URL object-based processing for "${urlString}": ${e.message}. Current decodedUrl: "${decodedUrl}". Original error:`, e); } return decodedUrl; }
function getOutputDisplayObject(fullDisplayData) { if (!GSRS_App.currentSettings.hideDisabledFetchFields) { const completeObject = { position: fullDisplayData.position, title: GSRS_App.currentSettings.fetchTitle ? fullDisplayData.title : null, url: GSRS_App.currentSettings.fetchUrl ? fullDisplayData.url : null, siteName: GSRS_App.currentSettings.fetchSiteName ? fullDisplayData.siteName : null, breadcrumbs: GSRS_App.currentSettings.fetchBreadcrumbs ? fullDisplayData.breadcrumbs : null, description: GSRS_App.currentSettings.fetchDescription ? fullDisplayData.description : null, highlightedSnippets: (GSRS_App.currentSettings.fetchDescription && GSRS_App.currentSettings.fetchDescriptionKeywords) ? fullDisplayData.highlightedSnippets : null, originalDateText: (GSRS_App.currentSettings.fetchDescription && GSRS_App.currentSettings.fetchDateInfo) ? fullDisplayData.originalDateText : null, parsedDateISO: (GSRS_App.currentSettings.fetchDescription && GSRS_App.currentSettings.fetchDateInfo) ? fullDisplayData.parsedDateISO : null, }; return completeObject; } const output = {}; if (fullDisplayData.hasOwnProperty('position')) output.position = fullDisplayData.position; if (GSRS_App.currentSettings.fetchTitle) output.title = fullDisplayData.title; if (GSRS_App.currentSettings.fetchUrl) output.url = fullDisplayData.url; if (GSRS_App.currentSettings.fetchSiteName) output.siteName = fullDisplayData.siteName; if (GSRS_App.currentSettings.fetchBreadcrumbs) output.breadcrumbs = fullDisplayData.breadcrumbs; if (GSRS_App.currentSettings.fetchDescription) { output.description = fullDisplayData.description; if (GSRS_App.currentSettings.fetchDescriptionKeywords) { output.highlightedSnippets = fullDisplayData.highlightedSnippets; } if (GSRS_App.currentSettings.fetchDateInfo) { output.originalDateText = fullDisplayData.originalDateText; output.parsedDateISO = fullDisplayData.parsedDateISO; } } return output; }
// --- Enhanced testSelector ---
function testSelector(inputId, resultSpanId, previewId) {
const inputElement = document.getElementById(inputId);
const resultSpan = document.getElementById(resultSpanId);
const previewDiv = document.getElementById(previewId);
clearTimeout(GSRS_App.state.selectorTestHighlightTimeout);
document.querySelectorAll('.gsrs-selector-test-highlight').forEach(el => el.classList.remove('gsrs-selector-test-highlight'));
if (!inputElement || !resultSpan || !previewDiv) { console.error(`GSRS: Test selector UI elements not found. Input ID: ${inputId}, Result Span ID: ${resultSpanId}, Preview ID: ${previewId}`); if(resultSpan) { resultSpan.textContent = "Error!"; resultSpan.style.color = "red"; } if(previewDiv) { previewDiv.style.display = 'none'; previewDiv.innerHTML = ''; } return; }
const rawSelectorString = inputElement.value.trim();
previewDiv.style.display = 'none'; previewDiv.innerHTML = '';
if (!rawSelectorString) { resultSpan.textContent = "Selector is empty."; resultSpan.style.color = "red"; previewDiv.innerHTML = 'Selector is empty.'; previewDiv.style.display = 'block'; return; }
const isObserverSelector = inputId === 'gsrs-input-observer-selector';
const selectorsToTest = isObserverSelector ? rawSelectorString.split(',').map(s => s.trim()).filter(s => s) : [rawSelectorString];
let totalFound = 0;
let resultsTextArray = [];
let allMatchedElementsForPreview = [];
selectorsToTest.forEach((selector, index) => {
if (!selector) { if (isObserverSelector) resultsTextArray.push(`Sel ${index+1}: Empty`); return; }
try {
const elements = Array.from(document.querySelectorAll(selector));
const count = elements.length;
totalFound += count;
if (isObserverSelector) {
resultsTextArray.push(`Sel ${index+1} ("${selector.substring(0,15)}..."): ${count}`);
} else {
resultsTextArray.push(`Found: ${count}`);
}
if (count > 0) {
allMatchedElementsForPreview.push(...elements);
elements.forEach(el => el.classList.add('gsrs-selector-test-highlight'));
}
} catch (e) { if (isObserverSelector) { resultsTextArray.push(`Sel ${index+1} ("${selector.substring(0,15)}..."): Invalid`); } else { resultsTextArray = ["Invalid selector syntax!"]; totalFound = -1; previewDiv.innerHTML = ''; const errorStrong = document.createElement('strong'); errorStrong.textContent = 'Error:'; previewDiv.appendChild(errorStrong); previewDiv.appendChild(document.createTextNode(" " + e.message)); previewDiv.style.display = 'block'; console.error(`GSRS Test Selector Error for "${selector}":`, e.message); } }
});
resultSpan.textContent = resultsTextArray.join('; ');
resultSpan.style.color = totalFound > 0 ? "green" : (totalFound === 0 ? "orange" : "red");
if (allMatchedElementsForPreview.length > 0) {
if(GSRS_App.currentSettings.debugMode) console.log(`GSRS Test: Selector(s) "${rawSelectorString}" found ${totalFound} total elements. First previewable element:`, allMatchedElementsForPreview[0]);
previewDiv.innerHTML = '';
const MAX_PREVIEW_ITEMS = 5;
const itemsToPreview = allMatchedElementsForPreview.slice(0, MAX_PREVIEW_ITEMS);
itemsToPreview.forEach((el, idx) => {
const itemPreviewContainer = document.createElement('div');
itemPreviewContainer.style.borderBottom = idx < itemsToPreview.length - 1 ? '1px dashed #ccc' : 'none';
itemPreviewContainer.style.paddingBottom = '5px';
itemPreviewContainer.style.marginBottom = '5px';
const itemTitle = document.createElement('strong');
itemTitle.textContent = `Match ${idx + 1} (of ${totalFound > MAX_PREVIEW_ITEMS ? MAX_PREVIEW_ITEMS + ' shown' : totalFound}): <${el.tagName.toLowerCase()}${el.id ? '#' + el.id : ''}${el.className ? '.' + String(el.className).trim().replace(/\s+/g,'.') : ''}>`;
itemPreviewContainer.appendChild(itemTitle);
const innerTextStrong = document.createElement('strong');
innerTextStrong.textContent = 'InnerText (truncated):';
innerTextStrong.style.display = 'block'; innerTextStrong.style.marginTop = '3px';
itemPreviewContainer.appendChild(innerTextStrong);
const innerTextDiv = document.createElement('div');
innerTextDiv.style.maxHeight = '40px'; innerTextDiv.style.overflowY = 'auto'; innerTextDiv.style.background = '#e9e9e9'; innerTextDiv.style.padding = '2px';
const innerTextRaw = el.innerText || "";
innerTextDiv.textContent = innerTextRaw.length > 150 ? innerTextRaw.substring(0, 150) + "..." : innerTextRaw;
itemPreviewContainer.appendChild(innerTextDiv);
const outerHTMLStrong = document.createElement('strong');
outerHTMLStrong.textContent = 'OuterHTML (truncated):';
outerHTMLStrong.style.display = 'block'; outerHTMLStrong.style.marginTop = '3px';
itemPreviewContainer.appendChild(outerHTMLStrong);
const outerHTMLDiv = document.createElement('div');
outerHTMLDiv.style.maxHeight = '60px'; outerHTMLDiv.style.overflowY = 'auto'; outerHTMLDiv.style.background = '#e0e0e0'; outerHTMLDiv.style.padding = '2px';
const outerHTMLRaw = el.outerHTML || "";
outerHTMLDiv.textContent = outerHTMLRaw.length > 300 ? outerHTMLRaw.substring(0, 300) + "\n... (truncated)" : outerHTMLRaw;
itemPreviewContainer.appendChild(outerHTMLDiv);
previewDiv.appendChild(itemPreviewContainer);
});
if (totalFound > MAX_PREVIEW_ITEMS) {
const moreInfo = document.createElement('p');
moreInfo.textContent = `... and ${totalFound - MAX_PREVIEW_ITEMS} more match(es).`;
moreInfo.style.fontSize = '10px';
moreInfo.style.textAlign = 'center';
previewDiv.appendChild(moreInfo);
}
previewDiv.style.display = 'block';
} else if (totalFound === 0 && !resultsTextArray.some(s => s.includes("Invalid"))) {
if(GSRS_App.currentSettings.debugMode) console.log(`GSRS Test: Selector(s) "${rawSelectorString}" found 0 elements.`);
previewDiv.innerHTML = 'No elements matched this/these selector(s).';
previewDiv.style.display = 'block';
}
if (totalFound > 0) {
GSRS_App.state.selectorTestHighlightTimeout = setTimeout(() => {
document.querySelectorAll('.gsrs-selector-test-highlight').forEach(el => el.classList.remove('gsrs-selector-test-highlight'));
}, 3500);
}
}
// --- Global functions needing GSRS_App.state ---
function makeDraggable(element, handle) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const dragHandle = handle || element;
let currentTransition = '';
dragHandle.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
let target = e.target;
let isInteractive = false;
if (handle && handle.contains(target) && target !== handle) {
if (target.closest('button, input, select, textarea, a')) {
if (!(target.id === 'gsrs-settings-toggle-titlebar' || target.id === 'gsrs-minimize-btn' || target.id === 'gsrs-maximize-btn' || target.closest('#gsrs-settings-toggle-titlebar'))) {
isInteractive = true;
}
}
} else if (dragHandle === element) {
const nonDraggableSelectors = 'input, textarea, button, select, a, .gsrs-selector-test-preview, #gsrs-results-area, #gsrs-results-list-container, #gsrs-result-preview-area, .gsrs-copy-download-options label, .gsrs-view-toggle-buttons button, #gsrs-settings-panel > *:not(#gsrs-title-bar), .gsrs-list-item, .gsrs-action-format-link, #gsrs-context-menu, .gsrs-context-menu-item';
if (target.closest(nonDraggableSelectors) && target !== element && !target.closest('#gsrs-title-bar')) {
isInteractive = true;
}
}
if (isInteractive) {
if (GSRS_App.currentSettings.debugMode) console.log("GSRS Drag: Interactive element click, drag NOT initiated on:", target);
return;
}
if (GSRS_App.state.isMaximized) return;
e.preventDefault();
currentTransition = element.style.transition || '';
element.style.transition = 'none';
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
let newTop = element.offsetTop - pos2;
let newLeft = element.offsetLeft - pos1;
const panelWidth = element.offsetWidth;
const panelHeight = element.offsetHeight;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
if (element.style.left !== 'auto' && GSRS_App.state.originalPanelState.left !== 'auto') {
newLeft = Math.max(0, Math.min(newLeft, winWidth - panelWidth));
element.style.left = newLeft + "px";
element.style.right = 'auto';
} else {
let currentRight = parseFloat(getComputedStyle(element).right);
if (isNaN(currentRight)) { // If 'auto' or not set, calculate from offsetLeft
currentRight = winWidth - (element.offsetLeft + panelWidth);
}
let newRightValue = currentRight + pos1;
newRightValue = Math.max(0, Math.min(newRightValue, winWidth - panelWidth));
element.style.right = newRightValue + "px";
element.style.left = 'auto';
}
const actualHandleHeight = (dragHandle.offsetHeight > 0) ? dragHandle.offsetHeight : 20;
const MIN_VISIBLE_HANDLE_PART = 10;
if (panelHeight <= winHeight) {
newTop = Math.max(0, Math.min(newTop, winHeight - panelHeight));
} else {
newTop = Math.max(MIN_VISIBLE_HANDLE_PART - actualHandleHeight, Math.min(newTop, winHeight - MIN_VISIBLE_HANDLE_PART));
}
element.style.top = newTop + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
if (element) element.style.transition = currentTransition;
if (!GSRS_App.state.isMaximized) {
GSRS_App.settingsManager.saveUIPrefs();
}
}
}
function toggleMaximizePanel() {
const uiContainer = GSRS_App.uiElements.uiContainer; if (!uiContainer) return;
const maximizeBtn = document.getElementById('gsrs-maximize-btn');
const contentWrapper = document.getElementById('gsrs-content-wrapper');
const prevTransition = getComputedStyle(uiContainer).transition;
uiContainer.style.transition = 'none';
if (GSRS_App.state.isMaximized) {
uiContainer.style.width = GSRS_App.state.originalPanelState.width || GSRS_App.DEFAULT_SETTINGS.uiPanelWidth;
uiContainer.style.height = GSRS_App.state.originalPanelState.height || '';
uiContainer.style.top = GSRS_App.state.originalPanelState.top || GSRS_App.DEFAULT_SETTINGS.uiPanelTop;
if (GSRS_App.state.originalPanelState.left && GSRS_App.state.originalPanelState.left !== 'auto') {
uiContainer.style.left = GSRS_App.state.originalPanelState.left;
uiContainer.style.right = 'auto';
} else if (GSRS_App.state.originalPanelState.right && GSRS_App.state.originalPanelState.right !== 'auto') {
uiContainer.style.right = GSRS_App.state.originalPanelState.right;
uiContainer.style.left = 'auto';
} else {
uiContainer.style.left = GSRS_App.DEFAULT_SETTINGS.uiPanelLeft;
uiContainer.style.right = GSRS_App.DEFAULT_SETTINGS.uiPanelRight;
}
uiContainer.classList.remove('gsrs-maximized-panel');
if (maximizeBtn) { maximizeBtn.innerHTML = '<span class="icon">🗖</span>'; maximizeBtn.title = 'Maximize Panel'; }
GSRS_App.state.isMaximized = false;
} else {
GSRS_App.state.originalPanelState = {
top: uiContainer.style.top || getComputedStyle(uiContainer).top,
left: uiContainer.style.left || getComputedStyle(uiContainer).left,
right: uiContainer.style.right || getComputedStyle(uiContainer).right,
width: uiContainer.style.width || getComputedStyle(uiContainer).width,
height: uiContainer.style.height || getComputedStyle(uiContainer).height
};
const winWidth = window.innerWidth; const winHeight = window.innerHeight; const padding = 20;
let newWidth = winWidth - (padding * 2); let newHeight = winHeight - (padding * 2);
newWidth = Math.max(newWidth, parseInt(GSRS_App.DEFAULT_SETTINGS.uiPanelWidth, 10));
newHeight = Math.max(newHeight, 480);
uiContainer.style.width = newWidth + 'px';
uiContainer.style.height = newHeight + 'px';
uiContainer.style.top = padding + 'px';
uiContainer.style.left = padding + 'px';
uiContainer.style.right = 'auto';
uiContainer.classList.add('gsrs-maximized-panel');
if (maximizeBtn) { maximizeBtn.innerHTML = '<span class="icon">🗗</span>'; maximizeBtn.title = 'Restore Panel Size'; }
GSRS_App.state.isMaximized = true;
}
void uiContainer.offsetWidth;
uiContainer.style.transition = prevTransition;
if (contentWrapper) {
const isMinimizedState = uiContainer.classList.contains('gsrs-minimized');
const currentDisplay = getComputedStyle(contentWrapper).display;
if (!isMinimizedState && currentDisplay === 'none') {
contentWrapper.style.display = 'none'; void contentWrapper.offsetHeight; contentWrapper.style.display = 'flex';
} else if (isMinimizedState) {
contentWrapper.style.display = 'none';
}
}
}
function toggleViewMode(mode) {
if (mode === GSRS_App.state.currentViewMode && GSRS_App.uiElements.uiContainer && !GSRS_App.uiElements.uiContainer.classList.contains('gsrs-view-just-toggled')) {
if (mode === 'list') GSRS_App.uiManager.populateResultsList();
return;
}
GSRS_App.state.currentViewMode = mode;
if (GSRS_App.uiElements.uiContainer) GSRS_App.uiElements.uiContainer.classList.add('gsrs-view-just-toggled');
const jsonViewBtn = document.getElementById('gsrs-view-toggle-json');
const listViewBtn = document.getElementById('gsrs-view-toggle-list');
const resultsTextArea = GSRS_App.uiElements.resultsTextArea;
const resultsListContainer = GSRS_App.uiElements.resultsListContainer;
const resultPreviewArea = GSRS_App.uiElements.resultPreviewArea;
if (GSRS_App.state.currentViewMode === 'list') {
if (resultsTextArea) resultsTextArea.style.display = 'none';
if (resultsListContainer) resultsListContainer.style.display = 'flex';
if (resultPreviewArea) { resultPreviewArea.style.display = GSRS_App.currentSettings.showPreviewInListMode ? 'block' : 'none'; }
if (jsonViewBtn) jsonViewBtn.classList.remove('active');
if (listViewBtn) listViewBtn.classList.add('active');
GSRS_App.uiManager.populateResultsList();
if (resultPreviewArea && (!GSRS_App.state.selectedResultListItem || (resultsListContainer && resultsListContainer.children.length === 0 && !resultsListContainer.textContent.includes("No results")) )) {
resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">Click an item from the list to see details.</p>';
}
} else { // JSON mode
clearAllPageHighlights(false);
if (resultsTextArea) resultsTextArea.style.display = 'block';
if (resultsListContainer) resultsListContainer.style.display = 'none';
if (resultPreviewArea) resultPreviewArea.style.display = 'none';
if (jsonViewBtn) jsonViewBtn.classList.add('active');
if (listViewBtn) listViewBtn.classList.remove('active');
GSRS_App.uiManager.updateResultsDisplay();
}
GM_setValue('gsrs_lastViewMode', GSRS_App.state.currentViewMode);
if (GSRS_App.uiElements.uiContainer) setTimeout(() => GSRS_App.uiElements.uiContainer.classList.remove('gsrs-view-just-toggled'), 0);
}
// --- Enhanced Context Menu Logic ---
function handleContextMenuAction(action) {
if (!GSRS_App.state.currentContextMenuItemData || !action) {
if(GSRS_App.currentSettings.debugMode) console.warn("GSRS ContextMenu: No item data or action specified for action:", action);
return;
}
const displayData = getOutputDisplayObject(GSRS_App.state.currentContextMenuItemData.display);
const internalData = GSRS_App.state.currentContextMenuItemData.internal;
const domElement = GSRS_App.state.currentContextMenuItemData.domElement;
if(GSRS_App.currentSettings.debugMode) console.log(`GSRS ContextMenu: Action "${action}" on item:`, GSRS_App.state.currentContextMenuItemData);
clearAllPageHighlights(false);
switch (action) {
case 'copy-json': GM_setClipboard(JSON.stringify(displayData, null, 2)); GSRS_App.uiManager.showUIMessage('Item JSON copied!', 'success', 2000); break;
case 'copy-title': if (displayData.title && displayData.title !== "Extraction Failed" && displayData.title !== "Title Selector Failed") { GM_setClipboard(displayData.title); GSRS_App.uiManager.showUIMessage('Title copied!', 'success', 2000); } else { GSRS_App.uiManager.showUIMessage('No valid title to copy.', 'error', 2000); } break;
case 'copy-url':
let urlToCopy = (displayData.url && displayData.url !== "Extraction Failed" && displayData.url !== "URL Extraction Failed") ? displayData.url : null;
if (!urlToCopy && internalData.rawUrl && internalData.rawUrl !== "Extraction Failed" && internalData.rawUrl !== "URL Extraction Failed") { urlToCopy = decodeUrlIfEnabled(internalData.rawUrl); }
if (urlToCopy) { GM_setClipboard(urlToCopy); GSRS_App.uiManager.showUIMessage('URL copied!', 'success', 2000); }
else { GSRS_App.uiManager.showUIMessage('No valid URL to copy.', 'error', 2000); }
break;
case 'copy-description':
if (displayData.description && displayData.description !== "Extraction Failed" && displayData.description !== "Desc Extraction Failed") { GM_setClipboard(displayData.description); GSRS_App.uiManager.showUIMessage('Description copied!', 'success', 2000); }
else { GSRS_App.uiManager.showUIMessage('No valid description to copy.', 'error', 2000); }
break;
case 'open-url':
if (internalData.rawUrl && internalData.rawUrl !== "Extraction Failed" && internalData.rawUrl !== "URL Extraction Failed" && !internalData.rawUrl.startsWith('javascript:')) {
window.open(internalData.rawUrl, '_blank');
} else {
GSRS_App.uiManager.showUIMessage('Invalid or no URL to open.', 'error', 2000);
}
break;
case 'highlight-on-page':
if (domElement && document.body.contains(domElement)) {
clearTimeout(GSRS_App.state.contextMenuHighlightTimeout);
if (GSRS_App.state.lastListItemHighlightedElement && GSRS_App.state.lastListItemHighlightedElement !== domElement && document.body.contains(GSRS_App.state.lastListItemHighlightedElement)) {
GSRS_App.state.lastListItemHighlightedElement.classList.remove('gsrs-list-item-page-highlight');
}
domElement.classList.remove('gsrs-highlighted', 'gsrs-context-highlight', 'gsrs-list-item-page-highlight');
domElement.classList.add('gsrs-context-highlight');
domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
GSRS_App.state.lastListItemHighlightedElement = domElement;
GSRS_App.state.contextMenuHighlightTimeout = setTimeout(() => {
if (domElement && document.body.contains(domElement)) domElement.classList.remove('gsrs-context-highlight');
if (GSRS_App.state.lastListItemHighlightedElement === domElement) GSRS_App.state.lastListItemHighlightedElement = null;
}, 3500);
GSRS_App.uiManager.showUIMessage('Item highlighted on page.', 'success', 2000);
} else {
GSRS_App.uiManager.showUIMessage('Could not find item on page to highlight.', 'error', 2000);
if (GSRS_App.currentSettings.debugMode) console.warn("GSRS ContextMenu: domElement not found or not in body for highlight.", domElement);
}
break;
default:
if(GSRS_App.currentSettings.debugMode) console.warn(`GSRS ContextMenu: Unknown action "${action}"`);
}
if (GSRS_App.uiElements.contextMenuElement) GSRS_App.uiElements.contextMenuElement.style.display = 'none';
}
function handleResultListItemContextMenu(event) {
const contextMenuElement = GSRS_App.uiElements.contextMenuElement;
if (!contextMenuElement) return;
event.preventDefault();
GSRS_App.state.currentContextMenuItemData = null;
const listItem = event.target.closest('.gsrs-list-item');
if (!listItem || !listItem.dataset.originalFullArrayIndex) {
contextMenuElement.style.display = 'none';
return;
}
const originalIndex = parseInt(listItem.dataset.originalFullArrayIndex, 10);
if (originalIndex >= 0 && originalIndex < GSRS_App.allResults.length) {
GSRS_App.state.currentContextMenuItemData = GSRS_App.allResults[originalIndex];
} else {
contextMenuElement.style.display = 'none';
return;
}
if (!GSRS_App.state.currentContextMenuItemData) {
contextMenuElement.style.display = 'none';
return;
}
const menuWidth = contextMenuElement.offsetWidth; const menuHeight = contextMenuElement.offsetHeight;
const windowWidth = window.innerWidth; const windowHeight = window.innerHeight;
let x = event.clientX; let y = event.clientY;
if (x + menuWidth > windowWidth) { x = windowWidth - menuWidth - 5; }
if (y + menuHeight > windowHeight) { y = windowHeight - menuHeight - 5; }
x = Math.max(0, x); y = Math.max(0, y);
contextMenuElement.style.left = `${x}px`; contextMenuElement.style.top = `${y}px`;
contextMenuElement.style.display = 'block';
const openUrlItem = contextMenuElement.querySelector('[data-action="open-url"]');
const highlightItem = contextMenuElement.querySelector('[data-action="highlight-on-page"]');
const copyDescItem = contextMenuElement.querySelector('[data-action="copy-description"]');
const copyTitleItem = contextMenuElement.querySelector('[data-action="copy-title"]');
const copyUrlItem = contextMenuElement.querySelector('[data-action="copy-url"]');
const internalData = GSRS_App.state.currentContextMenuItemData.internal;
const displayData = GSRS_App.state.currentContextMenuItemData.display;
if (openUrlItem) {
const urlIsInvalid = !internalData.rawUrl || internalData.rawUrl === "Extraction Failed" || internalData.rawUrl === "URL Extraction Failed" || internalData.rawUrl.startsWith('javascript:');
openUrlItem.classList.toggle('gsrs-cm-disabled', urlIsInvalid);
}
if (highlightItem) {
highlightItem.classList.toggle('gsrs-cm-disabled', !GSRS_App.state.currentContextMenuItemData.domElement || !document.body.contains(GSRS_App.state.currentContextMenuItemData.domElement));
}
if(copyDescItem){
const descIsInvalid = !displayData.description || displayData.description === "Extraction Failed" || displayData.description === "Desc Extraction Failed";
copyDescItem.classList.toggle('gsrs-cm-disabled', descIsInvalid);
}
if(copyTitleItem){
const titleIsInvalid = !displayData.title || displayData.title === "Extraction Failed" || displayData.title === "Title Selector Failed";
copyTitleItem.classList.toggle('gsrs-cm-disabled', titleIsInvalid);
}
if(copyUrlItem){
const displayUrlIsInvalid = !displayData.url || displayData.url === "Extraction Failed" || displayData.url === "URL Extraction Failed";
const rawUrlIsInvalid = !internalData.rawUrl || internalData.rawUrl === "Extraction Failed" || internalData.rawUrl === "URL Extraction Failed";
copyUrlItem.classList.toggle('gsrs-cm-disabled', displayUrlIsInvalid && rawUrlIsInvalid);
}
}
function populateResultsList() {
const resultsListContainer = GSRS_App.uiElements.resultsListContainer; if (!resultsListContainer) return;
clearAllPageHighlights(false);
resultsListContainer.innerHTML = '';
const resultsToDisplay = (GSRS_App.uiElements.filterInput && GSRS_App.uiElements.filterInput.value.trim() !== "") ? GSRS_App.filteredResults : GSRS_App.allResults;
if (resultsToDisplay.length === 0) {
resultsListContainer.innerHTML = '<div style="text-align:center; color:#777; padding:10px 0;">No results to display.</div>';
if (GSRS_App.uiElements.resultPreviewArea) { GSRS_App.uiElements.resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">No results to preview.</p>'; }
return;
}
const fragment = document.createDocumentFragment();
let currentSelectedOriginalResult = null;
if(GSRS_App.state.selectedResultListItem && GSRS_App.state.selectedResultListItem.dataset.originalFullArrayIndex) {
const originalIdx = parseInt(GSRS_App.state.selectedResultListItem.dataset.originalFullArrayIndex, 10);
if(originalIdx >= 0 && originalIdx < GSRS_App.allResults.length) {
if (resultsToDisplay.includes(GSRS_App.allResults[originalIdx])) {
currentSelectedOriginalResult = GSRS_App.allResults[originalIdx];
} else {
GSRS_App.state.selectedResultListItem = null;
if (GSRS_App.uiElements.resultPreviewArea) GSRS_App.uiElements.resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">Click an item from the list to see details.</p>';
}
}
}
resultsToDisplay.forEach((item, indexInCurrentList) => {
const listItem = document.createElement('div'); listItem.className = 'gsrs-list-item';
const originalIndex = GSRS_App.allResults.indexOf(item);
const displayObjForTitle = getOutputDisplayObject(item.display);
listItem.textContent = `[${displayObjForTitle.position || (originalIndex + 1)}] ${displayObjForTitle.title || item.internal.rawTitle || 'N/A'}`;
listItem.dataset.resultIndexInCurrentList = indexInCurrentList;
if (originalIndex !== -1) { listItem.dataset.originalFullArrayIndex = originalIndex; }
listItem.addEventListener('click', handleResultListItemClick);
listItem.addEventListener('contextmenu', handleResultListItemContextMenu);
fragment.appendChild(listItem);
if (item === currentSelectedOriginalResult) {
listItem.classList.add('selected');
GSRS_App.state.selectedResultListItem = listItem;
if (GSRS_App.currentSettings.highlightListItemOnPage && item.domElement && document.body.contains(item.domElement)) {
if (GSRS_App.state.lastListItemHighlightedElement && GSRS_App.state.lastListItemHighlightedElement !== item.domElement) {
GSRS_App.state.lastListItemHighlightedElement.classList.remove('gsrs-list-item-page-highlight');
}
item.domElement.classList.add('gsrs-list-item-page-highlight');
GSRS_App.state.lastListItemHighlightedElement = item.domElement;
}
}
});
resultsListContainer.appendChild(fragment);
if (!GSRS_App.state.selectedResultListItem && GSRS_App.uiElements.resultPreviewArea && resultsToDisplay.length > 0 && GSRS_App.currentSettings.showPreviewInListMode) {
GSRS_App.uiElements.resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">Click an item from the list to see details.</p>';
} else if (!GSRS_App.currentSettings.showPreviewInListMode && GSRS_App.uiElements.resultPreviewArea) {
GSRS_App.uiElements.resultPreviewArea.innerHTML = '';
}
}
function handleResultListItemClick(event) {
const resultPreviewArea = GSRS_App.uiElements.resultPreviewArea;
if (!resultPreviewArea && !GSRS_App.currentSettings.highlightListItemOnPage) return;
clearAllPageHighlights(false);
const listItem = event.currentTarget;
let resultItem = null;
const currentResultsSource = (GSRS_App.uiElements.filterInput && GSRS_App.uiElements.filterInput.value.trim() !== "") ? GSRS_App.filteredResults : GSRS_App.allResults;
if (listItem.dataset.originalFullArrayIndex) {
const originalIndex = parseInt(listItem.dataset.originalFullArrayIndex, 10);
if (originalIndex >= 0 && originalIndex < GSRS_App.allResults.length) {
if (currentResultsSource.includes(GSRS_App.allResults[originalIndex])) {
resultItem = GSRS_App.allResults[originalIndex];
}
}
}
if (!resultItem) {
const resultIndexInCurrentList = parseInt(listItem.dataset.resultIndexInCurrentList, 10);
if (resultIndexInCurrentList >= 0 && resultIndexInCurrentList < currentResultsSource.length) {
resultItem = currentResultsSource[resultIndexInCurrentList];
}
}
if (resultItem) {
const resultData = getOutputDisplayObject(resultItem.display);
const originalIndexGlobal = GSRS_App.allResults.indexOf(resultItem);
if (resultPreviewArea && GSRS_App.currentSettings.showPreviewInListMode) {
let previewHTML = `<p><strong>Position:</strong> ${resultData.position || (originalIndexGlobal + 1)}</p>`;
if (resultData.hasOwnProperty('title')) { previewHTML += `<p><strong>Title:</strong> ${resultData.title ? resultData.title.replace(/</g, '<') : 'Not Fetched/Available'}</p>`; }
else if (resultItem.internal.rawTitle && resultItem.internal.rawTitle !== "Extraction Failed" && resultItem.internal.rawTitle !== "Title Selector Failed") { previewHTML += `<p><strong>Title (raw):</strong> ${resultItem.internal.rawTitle.replace(/</g, '<')}</p>`; }
if (resultData.hasOwnProperty('url')) { const urlToDisplayInText = (resultData.url && resultData.url !== "Extraction Failed" && resultData.url !== "URL Extraction Failed") ? resultData.url : "Not Fetched/Available"; const hrefForLink = (resultItem.internal.rawUrl && resultItem.internal.rawUrl !== "Extraction Failed" && resultItem.internal.rawUrl !== "URL Extraction Failed") ? resultItem.internal.rawUrl : '#'; previewHTML += `<p><strong>URL:</strong> ${resultData.url ? `<a href="${hrefForLink}" target="_blank" title="Raw URL: ${hrefForLink}">${urlToDisplayInText.replace(/</g, '<')}</a>` : urlToDisplayInText}</p>`; }
if (resultData.hasOwnProperty('siteName')) { previewHTML += `<p><strong>Site Name:</strong> ${resultData.siteName ? resultData.siteName.replace(/</g, '<') : 'Not Fetched/Available'}</p>`; }
if (resultData.hasOwnProperty('breadcrumbs')) { previewHTML += `<p><strong>Breadcrumbs:</strong> ${resultData.breadcrumbs ? resultData.breadcrumbs.replace(/</g, '<') : 'Not Fetched/Available'}</p>`; }
if (resultData.hasOwnProperty('originalDateText') || resultData.hasOwnProperty('parsedDateISO')) { let dateDisplay = "Not Fetched/Available"; if (resultData.originalDateText) { dateDisplay = resultData.originalDateText.replace(/</g, '<'); if (resultData.parsedDateISO) { try { const d = new Date(resultData.parsedDateISO); if (!isNaN(d.getTime())) { dateDisplay += ` (Parsed: ${d.toLocaleString()})`; } } catch (e) {} } } else if (resultData.parsedDateISO) { try { const d = new Date(resultData.parsedDateISO); if (!isNaN(d.getTime())) { dateDisplay = `(Parsed: ${d.toLocaleString()})`; } else { dateDisplay = "Invalid Parsed Date"; } } catch (e) { dateDisplay = "Error parsing date"; } } previewHTML += `<p><strong>Date Info:</strong> ${dateDisplay}</p>`; }
if (resultData.hasOwnProperty('description')) { previewHTML += `<p><strong>Description:</strong> ${resultData.description ? resultData.description.replace(/</g, '<') : 'Not Fetched/Available'}</p>`; }
if (resultData.hasOwnProperty('highlightedSnippets') && resultData.highlightedSnippets) { previewHTML += `<p><strong>Keywords:</strong> ${resultData.highlightedSnippets.replace(/</g, '<')}</p>`; }
resultPreviewArea.innerHTML = previewHTML;
}
if (GSRS_App.state.selectedResultListItem && GSRS_App.state.selectedResultListItem !== listItem) { GSRS_App.state.selectedResultListItem.classList.remove('selected'); }
listItem.classList.add('selected');
GSRS_App.state.selectedResultListItem = listItem;
if (GSRS_App.currentSettings.highlightListItemOnPage && resultItem.domElement && document.body.contains(resultItem.domElement)) {
resultItem.domElement.classList.remove('gsrs-highlighted', 'gsrs-context-highlight');
resultItem.domElement.classList.add('gsrs-list-item-page-highlight');
resultItem.domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
GSRS_App.state.lastListItemHighlightedElement = resultItem.domElement;
}
} else {
if (resultPreviewArea && GSRS_App.currentSettings.showPreviewInListMode) { resultPreviewArea.innerHTML = '<p style="color: red;">Error: Could not retrieve result details.</p>'; }
if (GSRS_App.state.selectedResultListItem) GSRS_App.state.selectedResultListItem.classList.remove('selected');
GSRS_App.state.selectedResultListItem = null;
}
}
function handleStartParse() {
if (GSRS_App.currentSettings.debugMode) console.log("%cGSRS: handleStartParse called.", "color: orange; font-weight: bold;");
GSRS_App.allResults = []; GSRS_App.filteredResults = [];
const filterInput = GSRS_App.uiElements.filterInput;
if (filterInput) { filterInput.value = ''; GSRS_App.currentSettings.lastFilterTerm = ''; GM_setValue('gsrs_lastFilterTerm', ''); filterInput.classList.remove('gsrs-filter-active'); }
if (GSRS_App.state.selectedResultListItem) {GSRS_App.state.selectedResultListItem.classList.remove('selected'); GSRS_App.state.selectedResultListItem = null;}
const resultPreviewArea = GSRS_App.uiElements.resultPreviewArea;
if(resultPreviewArea) resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">Click an item from the list to see details.</p>';
clearAllPageHighlights(true);
try { document.querySelectorAll('[data-gsrs-block-parsed="true"]').forEach(el => {el.removeAttribute('data-gsrs-block-parsed');}); document.querySelectorAll('[data-gsrs-title-processed="true"]').forEach(el => {el.removeAttribute('data-gsrs-title-processed');}); }
catch (e) { console.warn(`GSRS: Issue clearing parsed flags. Error: ${e.message}`); }
const initialFoundCount = processPageForResults(document);
if (GSRS_App.allResults.length === 0 && document.querySelectorAll(GSRS_App.currentSettings.titleSelector).length > 0) { const specificMessage = `Scrape initiated. Found 0 results. Title selector '${GSRS_App.currentSettings.titleSelector}' matched elements, but no valid result blocks were identified. Check parent block logic or exclusion rules.`; GSRS_App.uiManager.showUIMessage(specificMessage, 'error', 12000); if(GSRS_App.currentSettings.debugMode) console.warn(`GSRS: ${specificMessage}`); }
else if (GSRS_App.allResults.length === 0 && document.querySelectorAll(GSRS_App.currentSettings.titleSelector).length === 0 && GSRS_App.uiElements.uiMessageDiv && !GSRS_App.uiElements.uiMessageDiv.textContent.includes("Warning: Title selector")) { const specificMessage = `Scrape initiated. Found 0 results. Title selector '${GSRS_App.currentSettings.titleSelector}' found NO elements. Please check your title selector.`; GSRS_App.uiManager.showUIMessage(specificMessage, 'error', 12000); if(GSRS_App.currentSettings.debugMode) console.warn(`GSRS: ${specificMessage}`); }
else if (GSRS_App.allResults.length > 0) { GSRS_App.uiManager.showUIMessage(`Scraped ${GSRS_App.allResults.length} results from current page.`, 'success'); }
else { GSRS_App.uiManager.showUIMessage('Scrape initiated. No results found on current page.', 'success', 4000); }
GSRS_App.uiManager.updateResultsDisplay();
GSRS_App.uiManager.updateObserverStatus(false); // Ensure observer status reflects no active dynamic detection
setupMutationObserver(); // This will just log that MO is disabled
const startButton = document.getElementById('gsrs-start-btn'); if (startButton) startButton.textContent = 'Re-Scrape Page';
}
function handleFilterResults() {
const filterInput = GSRS_App.uiElements.filterInput;
const searchTerm = filterInput ? filterInput.value.toLowerCase().trim() : "";
clearTimeout(GSRS_App.state.filterTimeout);
GSRS_App.state.filterTimeout = setTimeout(() => {
GSRS_App.currentSettings.lastFilterTerm = searchTerm;
GM_setValue('gsrs_lastFilterTerm', searchTerm);
if (GSRS_App.currentSettings.debugMode) console.log("GSRS: Saved filter term:", searchTerm);
}, 500);
if (filterInput) { if (searchTerm) filterInput.classList.add('gsrs-filter-active'); else filterInput.classList.remove('gsrs-filter-active'); }
if (!searchTerm) {
GSRS_App.filteredResults = [];
} else {
GSRS_App.filteredResults = GSRS_App.allResults.filter(item => {
const displayItem = getOutputDisplayObject(item.display); if (!displayItem) return false;
let match = false;
if (displayItem.hasOwnProperty('title') && displayItem.title && typeof displayItem.title === 'string') match = match || displayItem.title.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('url') && displayItem.url && typeof displayItem.url === 'string') match = match || displayItem.url.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('siteName') && displayItem.siteName && typeof displayItem.siteName === 'string') match = match || displayItem.siteName.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('breadcrumbs') && displayItem.breadcrumbs && typeof displayItem.breadcrumbs === 'string') match = match || displayItem.breadcrumbs.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('description') && displayItem.description && typeof displayItem.description === 'string') match = match || displayItem.description.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('highlightedSnippets') && displayItem.highlightedSnippets && typeof displayItem.highlightedSnippets === 'string') match = match || displayItem.highlightedSnippets.toLowerCase().includes(searchTerm);
if (displayItem.hasOwnProperty('originalDateText') && displayItem.originalDateText && typeof displayItem.originalDateText === 'string') match = match || displayItem.originalDateText.toLowerCase().includes(searchTerm);
return match;
});
}
clearAllPageHighlights(false);
if (GSRS_App.state.currentViewMode === 'list') { GSRS_App.uiManager.populateResultsList(); }
GSRS_App.uiManager.updateResultsDisplay();
}
function handleClearResults() {
const resultsTextArea = GSRS_App.uiElements.resultsTextArea;
const noResultsYet = GSRS_App.allResults.length === 0 && (!resultsTextArea || !resultsTextArea.value || (resultsTextArea.placeholder && resultsTextArea.value === '' && resultsTextArea.placeholder.toLowerCase().includes('no results scraped yet')));
const filterInput = GSRS_App.uiElements.filterInput;
if (noResultsYet && (!filterInput || !filterInput.value)) { GSRS_App.uiManager.showUIMessage('Already empty.', 'error', 2000); return; }
GSRS_App.allResults = []; GSRS_App.filteredResults = [];
if(filterInput) { filterInput.value = ''; GSRS_App.currentSettings.lastFilterTerm = ''; GM_setValue('gsrs_lastFilterTerm', ''); filterInput.classList.remove('gsrs-filter-active'); }
if (resultsTextArea) resultsTextArea.value = '';
const resultPreviewArea = GSRS_App.uiElements.resultPreviewArea;
if (resultPreviewArea) resultPreviewArea.innerHTML = '<p style="color: #777; text-align:center; margin-top: 20px;">Click an item from the list to see details.</p>';
if (GSRS_App.state.selectedResultListItem) { GSRS_App.state.selectedResultListItem.classList.remove('selected'); GSRS_App.state.selectedResultListItem = null; }
clearAllPageHighlights(true);
GSRS_App.uiManager.updateResultsDisplay();
GSRS_App.uiManager.showUIMessage('Results cleared.', 'success');
}
function updateActionButtonsState() {
const actionButtonBar = document.getElementById('gsrs-action-button-bar');
if (!actionButtonBar) { if (GSRS_App.currentSettings.debugMode && document.readyState === 'complete') { console.warn("GSRS: updateActionButtonsState - #gsrs-action-button-bar not found."); } return; }
const actionLinks = actionButtonBar.querySelectorAll('.gsrs-action-format-link');
if (!actionLinks || actionLinks.length === 0) { if (GSRS_App.currentSettings.debugMode && document.readyState === 'complete') { console.warn("GSRS: updateActionButtonsState - no .gsrs-action-format-link found."); } return; }
let resultsToConsider;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== "";
if (GSRS_App.state.copyDownloadTarget === 'all') {
resultsToConsider = GSRS_App.allResults;
} else {
resultsToConsider = isFilterActive ? GSRS_App.filteredResults : GSRS_App.allResults;
}
const noResults = resultsToConsider.length === 0;
actionLinks.forEach(link => { link.classList.toggle('gsrs-action-disabled', noResults); });
if (GSRS_App.currentSettings.debugMode) {
console.log(`GSRS: updateActionButtonsState - Target: ${GSRS_App.state.copyDownloadTarget}, FilterActive: ${isFilterActive}, ResultsToConsider: ${resultsToConsider.length}, NoResults: ${noResults}`);
actionLinks.forEach(link => { console.log(` - Link "${link.textContent}" (Action: ${link.dataset.action}, Format: ${link.dataset.format}): Disabled = ${link.classList.contains('gsrs-action-disabled')}`); });
}
}
function handleCopyOrDownloadMarkdown(actionType) {
let baseResultsToConsider = GSRS_App.allResults;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== '';
if (GSRS_App.state.copyDownloadTarget === 'current' && isFilterActive) { baseResultsToConsider = GSRS_App.filteredResults; }
const resultsToProcess = baseResultsToConsider.map(item => getOutputDisplayObject(item.display));
if (resultsToProcess.length === 0) { let msg = `No results to ${actionType} as Markdown.`; if (isFilterActive && GSRS_App.state.copyDownloadTarget === 'current') { msg = `Filter yielded no results to ${actionType} as Markdown.`; } GSRS_App.uiManager.showUIMessage(msg, 'error'); return; }
const markdownString = convertToMarkdownList(resultsToProcess);
if (actionType === 'copy') { GM_setClipboard(markdownString); GSRS_App.uiManager.showUIMessage(`Copied ${resultsToProcess.length} results as Markdown!`, 'success'); }
else if (actionType === 'download') {
const blob = new Blob([markdownString], {type: 'text/markdown;charset=utf-8;'}); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url;
const qParam = new URLSearchParams(window.location.search).get('q') || 'serp_results'; const filenameSafeQuery = qParam.replace(/[^a-z0-9_.-]/gi,'_').substring(0, 50);
a.download = `${filenameSafeQuery}_${new Date().toISOString().slice(0,10)}.md`;
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
GSRS_App.uiManager.showUIMessage(`Downloaded ${resultsToProcess.length} results as Markdown.`, 'success');
}
}
function handleCopyResults() {
let baseResultsToConsider = GSRS_App.allResults;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== '';
if (GSRS_App.state.copyDownloadTarget === 'current' && isFilterActive) { baseResultsToConsider = GSRS_App.filteredResults; }
const resultsToActOn = baseResultsToConsider.map(item => getOutputDisplayObject(item.display));
if (resultsToActOn.length > 0) { GM_setClipboard(JSON.stringify(resultsToActOn, null, 2)); GSRS_App.uiManager.showUIMessage(`Copied ${resultsToActOn.length} results as JSON!`, 'success'); }
else { let msg = 'No results to copy as JSON.'; if (isFilterActive && GSRS_App.state.copyDownloadTarget === 'current') { msg = 'Filter yielded no results to copy as JSON.'; } GSRS_App.uiManager.showUIMessage(msg, 'error'); }
}
function handleDownloadResults(format = 'json') {
let baseResultsToConsider = GSRS_App.allResults;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== '';
if (GSRS_App.state.copyDownloadTarget === 'current' && isFilterActive) { baseResultsToConsider = GSRS_App.filteredResults; }
const resultsToDownload = baseResultsToConsider.map(item => getOutputDisplayObject(item.display));
if (resultsToDownload.length === 0) { let msg = 'No results to download.'; if (isFilterActive && GSRS_App.state.copyDownloadTarget === 'current') { msg = 'Filter yielded no results to download.'; } GSRS_App.uiManager.showUIMessage(msg, 'error'); return; }
let dataString, blobType, fileExtension;
if (format === 'csv') {
const csvData = convertToCSV(resultsToDownload);
if (!csvData) { GSRS_App.uiManager.showUIMessage('No columns selected for CSV export. Please check settings.', 'error'); return; }
dataString = '\uFEFF' + csvData; blobType = 'text/csv;charset=utf-8;'; fileExtension = 'csv';
} else {
dataString = JSON.stringify(resultsToDownload, null, 2); blobType = 'application/json;charset=utf-8;'; fileExtension = 'json';
}
const blob = new Blob([dataString], {type: blobType}); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url;
const q = new URLSearchParams(window.location.search).get('q')||'serp_results'; const filenameSafeQuery = q.replace(/[^a-z0-9_.-]/gi,'_').substring(0, 50);
a.download = `${filenameSafeQuery}_${new Date().toISOString().slice(0,10)}.${fileExtension}`;
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
GSRS_App.uiManager.showUIMessage(`Downloaded ${resultsToDownload.length} results as ${fileExtension.toUpperCase()}.`, 'success');
}
function handleCopyUrls() {
let baseResultsToConsider = GSRS_App.allResults;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== '';
if (GSRS_App.state.copyDownloadTarget === 'current' && isFilterActive) { baseResultsToConsider = GSRS_App.filteredResults; }
const urls = baseResultsToConsider.map(item => { const displayObj = getOutputDisplayObject(item.display); return displayObj.url && displayObj.url !== "Extraction Failed" && displayObj.url !== "URL Extraction Failed" && !displayObj.url.startsWith('javascript:void(0)') ? displayObj.url : null; }).filter(url => url);
if (urls.length > 0) { let message = `Copied ${urls.length} URLs!`; if (urls.length === 1 && urls[0].length < 70) { message = `Copied URL: ${urls[0]}`; } GM_setClipboard(urls.join('\n')); GSRS_App.uiManager.showUIMessage(message, 'success', urls.length === 1 && urls[0].length < 70 ? 3000 : 2000); }
else { let msg = 'No valid URLs to copy.'; if (isFilterActive && GSRS_App.state.copyDownloadTarget === 'current') { msg = 'Filter yielded no URLs to copy.'; } else if (GSRS_App.allResults.length > 0 && urls.length === 0) { msg = 'No valid URLs found in current results (check fetch settings).'; } GSRS_App.uiManager.showUIMessage(msg, 'error'); }
}
function handleDownloadUrls() {
let baseResultsToConsider = GSRS_App.allResults;
const filterInput = GSRS_App.uiElements.filterInput;
const isFilterActive = filterInput && filterInput.value.trim() !== '';
if (GSRS_App.state.copyDownloadTarget === 'current' && isFilterActive) { baseResultsToConsider = GSRS_App.filteredResults; }
const urls = baseResultsToConsider.map(item => { const displayObj = getOutputDisplayObject(item.display); return displayObj.url && displayObj.url !== "Extraction Failed" && displayObj.url !== "URL Extraction Failed" && !displayObj.url.startsWith('javascript:void(0)') ? displayObj.url : null; }).filter(url => url);
if (urls.length === 0) { let msg = 'No valid URLs to download.'; if (isFilterActive && GSRS_App.state.copyDownloadTarget === 'current') { msg = 'Filter yielded no URLs to download.'; } else if (GSRS_App.allResults.length > 0 && urls.length === 0) { msg = 'No valid URLs found in current results (check fetch settings).'; } GSRS_App.uiManager.showUIMessage(msg, 'error'); return; }
const dataString = urls.join('\r\n'); const blobType = 'text/plain;charset=utf-8;'; const fileExtension = 'txt';
const blob = new Blob([dataString], {type: blobType}); const fileUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = fileUrl;
const q = new URLSearchParams(window.location.search).get('q')||'serp_urls'; const filenameSafeQuery = q.replace(/[^a-z0-9_.-]/gi,'_').substring(0, 50);
a.download = `${filenameSafeQuery}_${new Date().toISOString().slice(0,10)}_urls.${fileExtension}`;
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(fileUrl);
GSRS_App.uiManager.showUIMessage(`Downloaded ${urls.length} URLs as ${fileExtension.toUpperCase()}.`, 'success');
}
function updateResultsDisplay() {
const filterInput = GSRS_App.uiElements.filterInput;
const resultsCountSpan = GSRS_App.uiElements.resultsCountSpan;
const resultsTextArea = GSRS_App.uiElements.resultsTextArea;
const isFilterActive = filterInput && filterInput.value.trim() !== "";
const resultsToUseForDisplay = isFilterActive ? GSRS_App.filteredResults : GSRS_App.allResults;
if (resultsCountSpan) {
if (isFilterActive) { resultsCountSpan.textContent = `Showing ${resultsToUseForDisplay.length} of ${GSRS_App.allResults.length} results (filtered)`; }
else { resultsCountSpan.textContent = `Results: ${GSRS_App.allResults.length}`; }
}
if (GSRS_App.state.currentViewMode === 'json') {
if (resultsTextArea) {
const displayableJsonResults = resultsToUseForDisplay.map(item => getOutputDisplayObject(item.display));
if (displayableJsonResults.length === 0) {
if (isFilterActive && GSRS_App.allResults.length > 0) { resultsTextArea.value = 'No results match your filter.'; }
else { resultsTextArea.placeholder = 'No results scraped yet. Click "Scrape Page".'; resultsTextArea.value = ''; }
} else {
resultsTextArea.value = JSON.stringify(displayableJsonResults, null, 2);
resultsTextArea.placeholder = 'Scraped results will appear here...';
}
}
} else if (GSRS_App.state.currentViewMode === 'list') {
GSRS_App.uiManager.populateResultsList();
}
updateActionButtonsState();
}
function isPAAContainer(blockElement) { if (!blockElement) return false; const headingSpan = blockElement.querySelector(GSRS_App.INTERNAL_SELECTORS.relatedQuestionsBlockHeadingSpan); if (headingSpan && headingSpan.innerText) { const headingText = headingSpan.innerText.trim(); return GSRS_App.INTERNAL_SELECTORS.relatedQuestionsBlockTextIndicators.some(indicator => headingText.includes(indicator)); } return false; }
function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function _extractTitleAndUrl(resultBlock, debug, position) { let rawTitle = "Title Selector Failed"; let rawUrl = "URL Extraction Failed"; const titleElement = resultBlock.querySelector(GSRS_App.currentSettings.titleSelector); if (titleElement?.innerText) { rawTitle = titleElement.innerText.trim(); const anchorElement = titleElement.closest(GSRS_App.INTERNAL_SELECTORS.anchorInTitle); if (anchorElement?.href && !anchorElement.href.startsWith('javascript:')) { rawUrl = anchorElement.href; } if (debug) console.log(`GSRS (extractTitleUrl) [Pos ${position}]: Title found: "${rawTitle.substring(0,50)}...", URL from title anchor: "${rawUrl}"`); } else { if (debug) console.log(`GSRS (extractTitleUrl) [Pos ${position}]: titleElement (selector: ${GSRS_App.currentSettings.titleSelector}) not found or no innerText.`); } if (rawUrl === "URL Extraction Failed" || rawUrl === "Extraction Failed") { const firstLinkInBlock = resultBlock.querySelector('a[href] > h3'); if (firstLinkInBlock?.parentElement?.href && !firstLinkInBlock.parentElement.href.startsWith('javascript:')) { rawUrl = firstLinkInBlock.parentElement.href; if (rawTitle === "Title Selector Failed" && firstLinkInBlock.innerText) rawTitle = firstLinkInBlock.innerText.trim(); if (debug) console.log(`GSRS (extractTitleUrl) [Pos ${position}]: Fallback URL (a > h3): "${rawUrl}", Title: "${rawTitle.substring(0,50)}..."`); } else { const genericLink = resultBlock.querySelector('h3 > a[href], div > a[href] > h3, div > div > a[href] > h3'); if(genericLink?.href && !genericLink.href.startsWith('javascript:')) { rawUrl = genericLink.href; const h3InsideGeneric = genericLink.querySelector('h3'); if (rawTitle === "Title Selector Failed" && h3InsideGeneric?.innerText) { rawTitle = h3InsideGeneric.innerText.trim(); } else if (rawTitle === "Title Selector Failed" && genericLink.closest('h3')?.innerText) { rawTitle = genericLink.closest('h3').innerText.trim(); } else if (rawTitle === "Title Selector Failed" && genericLink.innerText) { rawTitle = genericLink.innerText.trim().split('\n')[0]; } if (debug) console.log(`GSRS (extractTitleUrl) [Pos ${position}]: Fallback URL (genericLink): "${rawUrl}", Title: "${rawTitle.substring(0,50)}..."`); } } } if (debug && (rawUrl === "URL Extraction Failed" || rawUrl === "Extraction Failed")) console.log(`GSRS (extractTitleUrl) [Pos ${position}]: URL extraction failed after all fallbacks.`); if (rawUrl.startsWith('javascript:void(0)')) rawUrl = "URL Extraction Failed"; if (rawTitle === "Title Selector Failed" && rawUrl !== "URL Extraction Failed" && rawUrl !== "Extraction Failed") { const urlAnchor = resultBlock.querySelector(`a[href="${CSS.escape(rawUrl)}"]`); if (urlAnchor?.innerText) { let potentialTitle = urlAnchor.innerText.trim(); const h3InAnchor = urlAnchor.querySelector('h3'); if (h3InAnchor?.innerText) potentialTitle = h3InAnchor.innerText.trim(); else if (urlAnchor.closest('h3')?.innerText) potentialTitle = urlAnchor.closest('h3').innerText.trim(); if(potentialTitle) rawTitle = potentialTitle.split('\n')[0]; if (debug && rawTitle !== "Title Selector Failed") console.log(`GSRS (extractTitleUrl) [Pos ${position}]: Title recovered from URL context: "${rawTitle.substring(0,50)}..."`); } } return { rawTitle, rawUrl }; }
function _extractSiteName(resultBlock, rawUrl, debug, position) { if (!GSRS_App.currentSettings.fetchSiteName) return null; let siteNameText = "Extraction Failed"; const siteNameElement = resultBlock.querySelector(GSRS_App.INTERNAL_SELECTORS.siteNameSelector); if (siteNameElement?.innerText) { let tempSiteName = siteNameElement.innerText.trim(); if (tempSiteName.includes('http')) { try { if (rawUrl && rawUrl !== "URL Extraction Failed" && rawUrl !== "Extraction Failed") { const urlPart = new URL(rawUrl.startsWith('http') ? rawUrl : 'http://' + rawUrl); if (tempSiteName.toLowerCase().includes(urlPart.hostname.replace('www.','').toLowerCase())) { tempSiteName = tempSiteName.split(/·|>|\u203A/)[0].trim(); tempSiteName = tempSiteName.replace(urlPart.hostname, '').replace(/\(|\)/g,'').trim(); } } } catch(e){ if (debug) console.warn(`GSRS (extractSiteName) [Pos ${position}]: Error cleaning siteName from URL parts. Error: ${e.message}`); } } siteNameText = tempSiteName || "Extraction Failed"; if (debug) console.log(`GSRS (extractSiteName) [Pos ${position}]: Site name: "${siteNameText}"`); } else if (debug) { console.warn(`GSRS (extractSiteName) [Pos ${position}]: siteNameElement NOT found using selector: "${GSRS_App.INTERNAL_SELECTORS.siteNameSelector}" on block:`, resultBlock); siteNameText = null; } return siteNameText; }
function _extractBreadcrumbs(resultBlock, rawUrl, debug, position) { if (!GSRS_App.currentSettings.fetchBreadcrumbs) return null; let breadcrumbsText = "Extraction Failed"; const citeElement = resultBlock.querySelector(GSRS_App.INTERNAL_SELECTORS.citeDisplay); if (citeElement?.innerText) { let tempBreadcrumbs = citeElement.innerText.trim(); if (rawUrl && rawUrl !== "URL Extraction Failed" && rawUrl !== "Extraction Failed") { try { const urlObj = new URL(rawUrl.startsWith('http') ? rawUrl : 'http://' + rawUrl); let hostnamePattern = urlObj.hostname.replace('www.', ''); hostnamePattern = escapeRegExp(hostnamePattern); const hostnameRegex = new RegExp(`(https?:\\/\\/)?(www\\.)?${hostnamePattern}\\s*([›>/\\s]|$)`, 'gi'); tempBreadcrumbs = tempBreadcrumbs.replace(hostnameRegex, '').trim(); } catch (e) { if (debug) console.warn(`GSRS (extractBreadcrumbs) [Pos ${position}]: Error cleaning breadcrumbs from URL. Error: ${e.message}`); } } tempBreadcrumbs = tempBreadcrumbs.replace(/^[\s›>\/\-\|]+|[\s›>\/\-\|]+$/g, '').replace(/\s+/g, ' ').trim(); if (tempBreadcrumbs && tempBreadcrumbs.length > 1) { const BREADCRUMB_NOISE_PATTERNS = [ /^\d+([.,]\d+)*\s*(萬|千)?\s*個?(追蹤者|粉絲|的說法|回應|評論|評分|評價|觀看次數|月前|天前|小時前|分鐘前|年前)/i, /^\d+年\d+月\d+日/i, /^\d{1,2}\/\d{1,2}\/\d{2,4}/, /^\w+\s\d{1,2},\s\d{4}/i, /重要時刻/i, /^\d+:\d+(?::\d+)?$/ ]; if (BREADCRUMB_NOISE_PATTERNS.some(p => p.test(tempBreadcrumbs))) { breadcrumbsText = null; if (debug) console.log(`GSRS (extractBreadcrumbs) [Pos ${position}]: Breadcrumb candidate "${tempBreadcrumbs}" filtered by noise pattern.`); } else if (!tempBreadcrumbs.includes(' ') && tempBreadcrumbs.includes('.') && !tempBreadcrumbs.includes('›') && !tempBreadcrumbs.includes('>')) { try { new URL('http://' + tempBreadcrumbs); breadcrumbsText = null; } catch(e){ breadcrumbsText = tempBreadcrumbs; } } else { breadcrumbsText = tempBreadcrumbs; } } else { breadcrumbsText = null; } if (debug) console.log(`GSRS (extractBreadcrumbs) [Pos ${position}]: Breadcrumbs: "${breadcrumbsText}" (Original cite: "${citeElement.innerText.trim().substring(0,100)}")`); } else if (debug) { console.warn(`GSRS (extractBreadcrumbs) [Pos ${position}]: citeElement for breadcrumbs NOT found using selector: "${GSRS_App.INTERNAL_SELECTORS.citeDisplay}" on block:`, resultBlock); breadcrumbsText = null; } return breadcrumbsText; }
function _extractDescriptionDetails(resultBlock, debug, position) { if (!GSRS_App.currentSettings.fetchDescription) { return { descriptionText: null, highlightedSnippetsText: null, originalDateText: null, parsedDateISO: null }; } let descriptionText = "Desc Extraction Failed"; let highlightedSnippetsText = null; let originalDateText = null; let parsedDateISO = null; let fullDescriptionSourceText = null; let descriptionContainerElement = null; let foundBySelector = "None"; const descSelectorsAttemptOrder = [ { name: "directDescriptionContainer", selector: GSRS_App.INTERNAL_SELECTORS.directDescriptionContainer }, { name: "genericDescriptionContainer", selector: GSRS_App.INTERNAL_SELECTORS.genericDescriptionContainer }, { name: "videoDescriptionSelector", selector: GSRS_App.INTERNAL_SELECTORS.videoDescriptionSelector } ]; for (const attempt of descSelectorsAttemptOrder) { descriptionContainerElement = resultBlock.querySelector(attempt.selector); if (descriptionContainerElement) { foundBySelector = attempt.name; if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Desc container found by ${foundBySelector}. HTML (brief): ${descriptionContainerElement.outerHTML.substring(0,100)}`); break; } } if (!descriptionContainerElement) { const outerDescBlock = resultBlock.querySelector(GSRS_App.INTERNAL_SELECTORS.outerDescriptionBlockSelector); if (outerDescBlock) { if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: OuterDescBlock found, trying selectors inside it.`); for (const attempt of descSelectorsAttemptOrder) { descriptionContainerElement = outerDescBlock.querySelector(attempt.selector); if (descriptionContainerElement) { foundBySelector = `${attempt.name} (in outer)`; if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Desc container found by ${foundBySelector}. HTML (brief): ${descriptionContainerElement.outerHTML.substring(0,100)}`); break; } } } } if (descriptionContainerElement) { fullDescriptionSourceText = descriptionContainerElement.innerText.trim(); if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Raw innerText from container (${foundBySelector}): "${fullDescriptionSourceText.substring(0, 200)}..."`); if (GSRS_App.currentSettings.fetchDescriptionKeywords) { const keywordElements = descriptionContainerElement.querySelectorAll(GSRS_App.INTERNAL_SELECTORS.descriptionKeywordSelector); if (keywordElements.length > 0) { highlightedSnippetsText = Array.from(keywordElements).map(em => em.innerText.trim()).filter(text => text).join(' ... '); if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Highlighted snippets: "${highlightedSnippetsText}"`); } else if (debug) { console.log(`GSRS (extractDescDetails) [Pos ${position}]: No keyword elements found with selector: ${GSRS_App.INTERNAL_SELECTORS.descriptionKeywordSelector}`); } } } else { if (debug) console.warn(`GSRS (extractDescDetails) [Pos ${position}]: Description container NOT found. Searched with: ${descSelectorsAttemptOrder.map(s=>s.selector).join(' , ')} on block:`, resultBlock); descriptionText = null; } if (fullDescriptionSourceText) { let processedDescription = fullDescriptionSourceText; if (GSRS_App.currentSettings.fetchDateInfo) { const dateInfo = _extractDateFromDescription(processedDescription, descriptionContainerElement, debug, position); originalDateText = dateInfo.originalDateText; parsedDateISO = dateInfo.parsedDateISO; if(originalDateText) { processedDescription = dateInfo.remainingDescription; } } descriptionText = processedDescription.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); if (descriptionText === "" && fullDescriptionSourceText !== "") { if (originalDateText && fullDescriptionSourceText.toLowerCase().trim() === originalDateText.toLowerCase().trim()) { if (debug) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Description is empty because original text was just the date.`); descriptionText = null; } else if (originalDateText) { descriptionText = (fullDescriptionSourceText.length > originalDateText.length + 5) ? "Desc Extraction Failed" : null; if (debug && descriptionText === "Desc Extraction Failed") console.warn(`GSRS (extractDescDetails) [Pos ${position}]: Description empty after date removal, but original was more. Original: "${fullDescriptionSourceText.substring(0,50)}...", Date: "${originalDateText}"`); else if (debug && descriptionText === null) console.log(`GSRS (extractDescDetails) [Pos ${position}]: Description considered null after date removal (original likely date + minor surrounding text).`); } } else if (descriptionText === "" && fullDescriptionSourceText === "" ) { descriptionText = null; } if (debug && descriptionText !== "Desc Extraction Failed") console.log(`GSRS (extractDescDetails) [Pos ${position}]: Final descriptionText: "${(descriptionText||"").substring(0,100)}..."`); } else if (!descriptionContainerElement) { descriptionText = null; } return { descriptionText, highlightedSnippetsText, originalDateText, parsedDateISO }; }
function _extractDateFromDescription(currentDescription, descriptionContainerElement, debug, position) { let originalDateText = null; let parsedDateISO = null; let remainingDescription = currentDescription; const separatorPatternGeneral = /^\s*(?:—|-)\s+/; const datePrefixSpan = descriptionContainerElement ? descriptionContainerElement.querySelector(GSRS_App.INTERNAL_SELECTORS.datePrefixSpanSelector) : null; if (datePrefixSpan?.innerText) { const potentialDateStrFromSpan = datePrefixSpan.innerText.trim(); if (debug) console.log(`GSRS (_extractDate) [Pos ${position}]: Date prefix span found, text: "${potentialDateStrFromSpan}"`); if (remainingDescription.startsWith(potentialDateStrFromSpan)) { const textAfterSpan = remainingDescription.substring(potentialDateStrFromSpan.length); const separatorMatch = textAfterSpan.match(separatorPatternGeneral); if (separatorMatch) { originalDateText = potentialDateStrFromSpan; remainingDescription = textAfterSpan.substring(separatorMatch[0].length).trim(); if (debug) console.log(`GSRS (_extractDate SPAN) [Pos ${position}]: Date extracted: "${originalDateText}". Desc remaining: "${remainingDescription.substring(0,50)}..."`); } else { if (debug) console.log(`GSRS (_extractDate SPAN) [Pos ${position}]: Found span text "${potentialDateStrFromSpan}", but no standard separator in: "${textAfterSpan.substring(0,30)}"`); const tempParsedDate = parseDateStringPureJS(potentialDateStrFromSpan, new Date()); if (tempParsedDate) { originalDateText = potentialDateStrFromSpan; remainingDescription = textAfterSpan.trim(); if (debug) console.log(`GSRS (_extractDate SPAN, NO SEP) [Pos ${position}]: Date candidate from span: "${originalDateText}". Desc remaining: "${remainingDescription.substring(0,50)}..."`); } else if (debug) { console.log(`GSRS (_extractDate SPAN, NO SEP) [Pos ${position}]: Span text "${potentialDateStrFromSpan}" did not parse as date, not using it as date.`); } } } } if (!originalDateText && remainingDescription) { if (debug) console.log(`GSRS (_extractDate REGEX) [Pos ${position}]: Trying regex on: "${remainingDescription.substring(0,50)}..."`); const cjkAbsoluteDatePatterns = [ /^(\d{4}年\s*\d{1,2}月\s*\d{1,2}日?(?:\s*\d{1,2}:\d{1,2}(?::\d{1,2})?)?)\s*(?:—|-)\s+/i, /^((\d{1,2})月\s*(\d{1,2})日?)\s*(?:—|-)\s+/i, ]; const enAbsoluteDatePatterns = [ /^(([a-z.]{3,9})\s+(\d{1,2})(?:st|nd|rd|th)?(?:,\s*|\s+)(\d{4})(?:\s+\d{1,2}:\d{1,2}(?::\d{1,2})?\s*(?:am|pm)?)?)\s*(?:—|-)\s+/i, /^((\d{1,2})\s+([a-z.]{3,9})\s+(\d{4})(?:\s+\d{1,2}:\d{1,2}(?::\d{1,2})?\s*(?:am|pm)?)?)\s*(?:—|-)\s+/i, /^((\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[T\s]\d{1,2}:\d{1,2}(?::\d{1,2})?)?)\s*(?:—|-)\s+/i, /^((\d{1,2})[-/.](\d{1,2})[-/.](\d{4}))\s*(?:—|-)\s+/i, ]; const relativeDatePatterns = [ /^((?:\d+\s+)?(?:year|month|week|day|hour|minute|second)s?\s+ago)\s*(?:—|-)\s+/i, /^(yesterday|today|just now|now)\s*(?:—|-)\s+/i, /^(\d+\s*(?:年|ヶ月|个月|週間|週|日|天|時間|小時|分|分鐘)前)\s*(?:—|-)\s+/i, /^(昨日|たった今|今さっき|昨天|剛剛|剛)\s*(?:—|-)\s+/i, ]; const generalDatePrefixPatterns = [ ...cjkAbsoluteDatePatterns, ...enAbsoluteDatePatterns, ...relativeDatePatterns ]; for (const pattern of generalDatePrefixPatterns) { const match = remainingDescription.match(pattern); if (match && match[1]) { originalDateText = match[1].trim(); remainingDescription = remainingDescription.substring(match[0].length).trim(); if (debug) console.log(`GSRS (_extractDate REGEX) [Pos ${position}]: Date extracted: "${originalDateText}". Desc remaining: "${remainingDescription.substring(0,50)}..."`); break; } } } if (originalDateText) { const parsedDateObject = parseDateStringPureJS(originalDateText, new Date()); if (parsedDateObject) { parsedDateISO = parsedDateObject.toISOString(); if (debug) console.log(`GSRS (_extractDate PARSE) [Pos ${position}]: Parsed "${originalDateText}" to ISO: ${parsedDateISO}`); } else if (debug) { console.log(`GSRS (_extractDate PARSE) [Pos ${position}]: Failed to parse extracted date string "${originalDateText}" with PureJS.`); } } else if (debug) { if (currentDescription && currentDescription.trim() !== "") { console.log(`GSRS (_extractDate) [Pos ${position}]: No date string extracted from description starting with: "${currentDescription.substring(0,100)}..."`); } } return { originalDateText, parsedDateISO, remainingDescription }; }
function extractSingleResultElement(resultBlock, position) { if (!resultBlock || resultBlock.dataset.gsrsBlockParsed === 'true') { return null; } const debug = GSRS_App.currentSettings.debugMode; if (resultBlock.id && (resultBlock.id.startsWith('infy-scroll-divider-') || resultBlock.id === 'infy-scroll-loading' || resultBlock.id === 'infy-scroll-bottom')) { if (debug) console.log(`GSRS (extractSingleResultElement) [Pos ${position}]: Ignoring Infy Scroll helper element:`, resultBlock.id); resultBlock.dataset.gsrsBlockParsed = 'true'; return null; } if (debug) console.log(`%cGSRS (extractSingleResultElement): Processing block for position ${position}. Element:`, "color: purple; font-weight: bold;", resultBlock); const { rawTitle, rawUrl } = _extractTitleAndUrl(resultBlock, debug, position); const siteNameText = _extractSiteName(resultBlock, rawUrl, debug, position); const breadcrumbsText = _extractBreadcrumbs(resultBlock, rawUrl, debug, position); const descDetails = _extractDescriptionDetails(resultBlock, debug, position); if (rawTitle === "Title Selector Failed" && (rawUrl === "URL Extraction Failed" || rawUrl === "Extraction Failed")) { if (debug) console.log(`GSRS (extractSingleResultElement) [Pos ${position}]: Returning NULL - Title selector failed AND URL extraction also failed.`); return null; } resultBlock.dataset.gsrsBlockParsed = 'true'; if (GSRS_App.currentSettings.highlightParsed) { if (!resultBlock.classList.contains('gsrs-list-item-page-highlight') && !resultBlock.classList.contains('gsrs-context-highlight')) { resultBlock.classList.add('gsrs-highlighted'); setTimeout(() => { if (resultBlock && resultBlock.classList.contains('gsrs-highlighted') && !resultBlock.classList.contains('gsrs-list-item-page-highlight') && !resultBlock.classList.contains('gsrs-context-highlight')) { resultBlock.classList.remove('gsrs-highlighted'); } }, 2500); } } const displayUrl = decodeUrlIfEnabled(rawUrl === "URL Extraction Failed" ? "Extraction Failed" : rawUrl); if (debug) { console.log(`%cGSRS (extractSingleResultElement Result) [Pos ${position}]:%c \nTitle: "${(rawTitle === "Title Selector Failed" ? "Extraction Failed" : rawTitle).substring(0,30)}..." \nRaw URL: "${rawUrl}" \nDisplay URL: "${displayUrl}" \nSiteName: "${siteNameText}" \nBreadcrumbs: "${breadcrumbsText}" \nOriginal Date: "${descDetails.originalDateText}" \nParsed Date ISO: "${descDetails.parsedDateISO}" \nDescription: "${(descDetails.descriptionText||"N/A").substring(0,50)}..." \nKeywords: "${(descDetails.highlightedSnippetsText||"N/A").substring(0,30)}..."`, "color: green; font-weight: bold;", "color: green;"); } return { internal: { rawTitle: (rawTitle === "Title Selector Failed" ? "Extraction Failed" : rawTitle), rawUrl }, display: { position, title: (rawTitle === "Title Selector Failed" ? "Extraction Failed" : rawTitle), url: displayUrl, siteName: siteNameText, breadcrumbs: breadcrumbsText, description: descDetails.descriptionText, highlightedSnippets: descDetails.highlightedSnippetsText, originalDateText: descDetails.originalDateText, parsedDateISO: descDetails.parsedDateISO }, domElement: resultBlock }; }
function processPageForResults(rootElement = document) { if (GSRS_App.currentSettings.debugMode) console.log("%cGSRS: processPageForResults starting...", "color: blue; font-weight: bold;", "Root:", rootElement === document ? "document" : rootElement); let newResultsFoundInThisPass = 0; const titleQuerySelector = GSRS_App.currentSettings.titleSelector; if (GSRS_App.allResults.length === 0 && rootElement === document) { try { if (document.querySelectorAll(titleQuerySelector).length === 0) { GSRS_App.uiManager.showUIMessage(`Warning: Title selector "${titleQuerySelector}" found 0 elements on the page. Check selector settings.`, 'error', 10000); } } catch (e) { GSRS_App.uiManager.showUIMessage(`Error with title selector "${titleQuerySelector}": ${e.message}. Please correct it in settings.`, 'error', 10000); return 0; } } const titleElements = Array.from(rootElement.querySelectorAll(titleQuerySelector)) .filter(titleEl => { if (titleEl.dataset.gsrsTitleProcessed === 'true') return false; if (titleEl.closest(GSRS_App.INTERNAL_SELECTORS.individualRelatedQuestionPair)) { if (GSRS_App.currentSettings.debugMode) console.log(`GSRS (processPage): Filtering out PAA item title: "${titleEl.innerText.substring(0,30)}..."`, titleEl); titleEl.dataset.gsrsTitleProcessed = 'true'; return false; } return true; }); const potentialResultBlocks = new Set(); if (GSRS_App.currentSettings.debugMode) console.log(`GSRS (processPage): Found ${titleElements.length} potential title elements (after PAA filtering) in root:`, rootElement === document ? "document" : rootElement); titleElements.forEach((titleEl) => { if (GSRS_App.currentSettings.debugMode) console.log(`GSRS (processPage): Processing Title Element: "${titleEl.innerText.substring(0, 50)}..."`, titleEl); let bestCandidateBlock = null; for (const knownSelector of GSRS_App.INTERNAL_SELECTORS.potentialResultBlockSelectors) { const closestKnownBlock = titleEl.closest(knownSelector); if (closestKnownBlock) { bestCandidateBlock = closestKnownBlock; if (GSRS_App.currentSettings.debugMode) console.log(`GSRS (processPage): Title belongs to known block type "${knownSelector}"`, bestCandidateBlock); break; } } if (!bestCandidateBlock) { let tempCandidate = titleEl; const MAX_FALLBACK_TRACE = 4; for (let i = 0; i < MAX_FALLBACK_TRACE && tempCandidate.parentElement; i++) { tempCandidate = tempCandidate.parentElement; if (tempCandidate.tagName === 'DIV' && GSRS_App.INTERNAL_SELECTORS.dataAttributesForCandidate.some(attr => tempCandidate.hasAttribute(attr))) { if (tempCandidate.id !== 'search' && tempCandidate.id !== 'rso' && tempCandidate.id !== 'main' && tempCandidate.tagName.toLowerCase() !== 'body' && tempCandidate.tagName.toLowerCase() !== 'html') { bestCandidateBlock = tempCandidate; if (GSRS_App.currentSettings.debugMode) console.log(`GSRS (processPage): Title fallback found block with data attribute.`, bestCandidateBlock); break; } } } } if (bestCandidateBlock) { if (bestCandidateBlock.dataset.gsrsBlockParsed === 'true') { titleEl.dataset.gsrsTitleProcessed = 'true'; return; } let isExcluded = false; if (GSRS_App.INTERNAL_SELECTORS.carouselStructureIndicator && bestCandidateBlock.querySelector(GSRS_App.INTERNAL_SELECTORS.carouselStructureIndicator)) { isExcluded = true; if (GSRS_App.currentSettings.debugMode) console.log("GSRS (processPage exclude): Carousel structure detected in block.", bestCandidateBlock); } if (!isExcluded) { const closestKPShell = bestCandidateBlock.closest(GSRS_App.INTERNAL_SELECTORS.knowledgePanelSelector); if (closestKPShell) { const isCandidateTheKPShellItself = bestCandidateBlock.matches(GSRS_App.INTERNAL_SELECTORS.knowledgePanelSelector); const candidateHasKPCoreDirectChild = GSRS_App.INTERNAL_SELECTORS.knowledgePanelCoreContentDirectChild.split(',').some(sel => bestCandidateBlock.querySelector(sel.trim())); if (isCandidateTheKPShellItself || candidateHasKPCoreDirectChild) { isExcluded = true; if (GSRS_App.currentSettings.debugMode) console.log("GSRS (processPage exclude): Knowledge panel detected.", bestCandidateBlock); } } } if (!isExcluded && isPAAContainer(bestCandidateBlock)) { isExcluded = true; if (GSRS_App.currentSettings.debugMode) console.log("GSRS (processPage exclude): PAA container block detected.", bestCandidateBlock); bestCandidateBlock.querySelectorAll(GSRS_App.currentSettings.titleSelector).forEach(t => t.dataset.gsrsTitleProcessed = 'true'); } if (!isExcluded) { potentialResultBlocks.add(bestCandidateBlock); } else { bestCandidateBlock.dataset.gsrsBlockParsed = 'true'; } } titleEl.dataset.gsrsTitleProcessed = 'true'; }); potentialResultBlocks.forEach(blockElement => { if (blockElement.dataset.gsrsBlockParsed === 'true') return; const parsedResultContainer = extractSingleResultElement(blockElement, GSRS_App.allResults.length + 1); if (parsedResultContainer?.internal) { const { internal } = parsedResultContainer; let isDuplicate = false; if (internal.rawUrl && internal.rawUrl !== "URL Extraction Failed" && internal.rawUrl !== "Extraction Failed" && !internal.rawUrl.startsWith('javascript:void(0)')) { isDuplicate = GSRS_App.allResults.some(existingItem => existingItem.internal.rawUrl === internal.rawUrl); } else if (internal.rawTitle && internal.rawTitle !== "Title Selector Failed" && internal.rawTitle !== "Extraction Failed") { isDuplicate = GSRS_App.allResults.some(existingItem => existingItem.internal.rawTitle === internal.rawTitle && (!existingItem.internal.rawUrl || ["URL Extraction Failed", "Extraction Failed"].includes(existingItem.internal.rawUrl) || existingItem.internal.rawUrl.startsWith('javascript:void'))); } if (!isDuplicate) { parsedResultContainer.display.position = GSRS_App.allResults.length + 1; GSRS_App.allResults.push(parsedResultContainer); newResultsFoundInThisPass++; } else { if (GSRS_App.currentSettings.debugMode) console.log("GSRS (processPage): Duplicate result skipped:", internal.rawTitle, internal.rawUrl); blockElement.dataset.gsrsBlockParsed = 'true'; } } else { if (!blockElement.dataset.gsrsBlockParsed) blockElement.dataset.gsrsBlockParsed = 'true'; if (GSRS_App.currentSettings.debugMode && parsedResultContainer === null && !(blockElement.id && blockElement.id.startsWith('infy-scroll-'))) { console.log("GSRS (processPage): extractSingleResultElement returned null for block, marking as parsed.", blockElement); } } }); if (GSRS_App.currentSettings.debugMode) console.log(`GSRS: processPageForResults finished for root ${rootElement === document ? "document" : "node"}. New results found in this pass: ${newResultsFoundInThisPass}`); return newResultsFoundInThisPass; }
function clearAllPageHighlights(includeScrapeTimeHighlight = true) { if (GSRS_App.state.lastListItemHighlightedElement && document.body.contains(GSRS_App.state.lastListItemHighlightedElement)) { GSRS_App.state.lastListItemHighlightedElement.classList.remove('gsrs-list-item-page-highlight'); GSRS_App.state.lastListItemHighlightedElement.classList.remove('gsrs-context-highlight'); } GSRS_App.state.lastListItemHighlightedElement = null; document.querySelectorAll('.gsrs-context-highlight').forEach(el => el.classList.remove('gsrs-context-highlight')); clearTimeout(GSRS_App.state.contextMenuHighlightTimeout); document.querySelectorAll('.gsrs-list-item-page-highlight').forEach(el => el.classList.remove('gsrs-list-item-page-highlight')); if (includeScrapeTimeHighlight) { document.querySelectorAll('.gsrs-highlighted').forEach(el => el.classList.remove('gsrs-highlighted')); } }
// --- MutationObserver (Now Sidelined/Disabled) ---
function setupMutationObserver() {
if (GSRS_App.currentSettings.debugMode) {
console.log("GSRS: MutationObserver setup is disabled as dynamic loading support for InfyScroll is removed.");
}
if (GSRS_App.observer) {
try {
GSRS_App.observer.disconnect();
if (GSRS_App.currentSettings.debugMode) console.log("GSRS_Debug (ObserverSetup): Disconnected any pre-existing observer instance.");
} catch (e) { /* silent */ }
GSRS_App.observer = null;
}
GSRS_App.uiManager.updateObserverStatus(false); // Set status to inactive
}
function init() {
GSRS_App.settingsManager.load();
GSRS_App.uiManager.create();
GSRS_App.settingsManager.updateInputs();
GSRS_App.uiManager.toggleViewMode(GSRS_App.state.currentViewMode);
GSRS_App.uiManager.updateObserverStatus(false); // MO is inactive
// Initialize URL change detector (now informational only for debugging/logging)
if (GSRS_App.urlChangeDetector && typeof GSRS_App.urlChangeDetector.init === 'function') {
GSRS_App.urlChangeDetector.init();
} else if (GSRS_App.currentSettings.debugMode) {
console.error("GSRS: URLChangeDetector not found or init is not a function.");
}
// Perform an initial scrape on load.
// This is effectively what handleStartParse does for the first time.
if (GSRS_App.currentSettings.debugMode) console.log("GSRS (Init): Performing initial page scrape.");
handleStartParse(); // Scrape current page content on init
if(GSRS_App.currentSettings.lastFilterTerm) {
handleFilterResults();
} else {
// updateResultsDisplay is called within handleStartParse
}
setTimeout(() => {
if (GSRS_App.currentSettings.debugMode) console.log("GSRS: Delayed initial call to updateActionButtonsState from init().");
updateActionButtonsState();
}, 100);
if (GSRS_App.currentSettings.debugMode) console.log("%cGSRS: Init complete. Dynamic loading via MutationObserver/URL changes is disabled.", "color: orange; font-weight: bold;");
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();