0% found this document useful (0 votes)
19 views17 pages

Speedbooster

The document is a user script designed to enhance browsing speed on Android devices by implementing features such as lazy loading of images, infinite scrolling, and a customizable settings interface. It includes functionality for managing domain-specific configurations and fetching remote mapping rules. The script is encapsulated within a Shadow DOM to prevent interference with the page's JavaScript and provides a notification system for user feedback.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views17 pages

Speedbooster

The document is a user script designed to enhance browsing speed on Android devices by implementing features such as lazy loading of images, infinite scrolling, and a customizable settings interface. It includes functionality for managing domain-specific configurations and fetching remote mapping rules. The script is encapsulated within a Shadow DOM to prevent interference with the page's JavaScript and provides a notification system for user feedback.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 17

// ==UserScript==

// @name Adaptive Android Speed Boost Loader - Encapsulated & Enhanced


// @namespace http://tampermonkey.net/
// @version 1.6.0
// @description Speeds up Android Browse: Lazy loads images (incl. dynamic),
infinite scroll (Pagetual/custom rules), Shadow DOM UI, disable options, reset.
// @author Your Name / Gemini Upgrade / Pagetual Rules by hoothin
// @license MIT
// @match *://*/*
// @exclude about:*
// @exclude chrome:*
// @exclude opera:*
// @exclude edge:*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @connect raw.githubusercontent.com
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';

// --- Early Exit Checks ---


if (!/Android/i.test(navigator.userAgent)) return; // Only run on Android
// Avoid running on frames, only top-level window
if (window.self !== window.top) return;

// --- Constants ---


const SCRIPT_NAME = 'Adaptive Android Speed Boost Loader';
const PAGETUAL_RULES_URL =
'https://raw.githubusercontent.com/hoothin/UserScripts/master/Pagetual/
pagetualRules.json';
const SETTINGS_KEY = 'adaptiveSpeedBoostLoaderSettings_v1_6'; // Updated key
version
const DOMAIN_MAP_KEY = 'domainMappings_v1_6'; // Updated key version

// ------------------------------------------------------
// 1. UI Host & Shadow DOM Setup
// ------------------------------------------------------
const uiHost = document.createElement('div');
uiHost.id = 'asb-ui-host-' + Date.now(); // Unique ID just in case
// Append host early, even before body exists sometimes
(document.body || document.documentElement).appendChild(uiHost);
// Use closed mode for better encapsulation (prevents page JS from easily
accessing it)
const shadowRoot = uiHost.attachShadow({ mode: 'closed' });

// ------------------------------------------------------
// 2. Toast Notification System (Inside Shadow DOM)
// ------------------------------------------------------
let toastTimeout;
function showToast(message, duration = 3500, isError = false) {
let toast = shadowRoot.getElementById('asb-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'asb-toast';
shadowRoot.appendChild(toast); // Append toast inside shadow DOM
}
toast.textContent = message;
toast.className = isError ? 'asb-toast asb-toast-error' : 'asb-toast'; //
Use classes for styling
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';

clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(10px)';
// Optional: remove toast element after fade out? Can be reused
instead.
}, duration);
}

// ------------------------------------------------------
// 3. Configuration Structure & Defaults
// ------------------------------------------------------
const builtInConfig = {
lazyImagesEnabled: true,
autoNextPageEnabled: true,
contentSelector: 'body', // Fallback
nextPageSelector: null, // Fallback
disabled: false, // Default for a matched rule
isDisabled: false // Flag set after evaluation
};

const defaultSettings = {
globallyDisabled: false, // New global disable flag
lazyImagesEnabled: builtInConfig.lazyImagesEnabled,
autoNextPageEnabled: builtInConfig.autoNextPageEnabled,
imageMargin: 150,
remoteMappingURL: PAGETUAL_RULES_URL,
autoFetchRemoteMappings: true
};

// ------------------------------------------------------
// 4. Settings Management (Using GM_* functions)
// ------------------------------------------------------
let settings = GM_getValue(SETTINGS_KEY, defaultSettings);
function saveSettings(newSettings) {
// Ensure we don't save undefined values from checkboxes that might not
exist yet
const cleanSettings = {};
for (const key in newSettings) {
if (newSettings[key] !== undefined) {
cleanSettings[key] = newSettings[key];
}
}
settings = { ...settings, ...cleanSettings };
GM_setValue(SETTINGS_KEY, settings);
// console.log(`${SCRIPT_NAME}: Settings saved`, settings);
}

function getDomainMappings() {
return GM_getValue(DOMAIN_MAP_KEY, []);
}
function saveDomainMappings(mappings) {
if (!Array.isArray(mappings)) {
console.error(`${SCRIPT_NAME}: Attempted to save invalid domain
mappings (not an array).`);
showToast("Error: Domain mappings must be a JSON array.", 5000, true);
return false;
}
GM_setValue(DOMAIN_MAP_KEY, mappings);
// console.log(`${SCRIPT_NAME}: Domain mappings saved`, mappings);
return true;
}
function resetSettingsAndMappings() {
GM_deleteValue(SETTINGS_KEY);
GM_deleteValue(DOMAIN_MAP_KEY);
settings = defaultSettings; // Reset in-memory settings
// console.log(`${SCRIPT_NAME}: Settings and mappings reset to defaults.`);
}

// ------------------------------------------------------
// 5. Remote Domain Mapping Fetch & Conversion
// ------------------------------------------------------
async function fetchRemoteDomainMappings(remoteURL) {
if (!remoteURL) {
showToast("No remote URL provided in settings.", 4000, true);
return;
}
showToast(`Workspaceing rules from ${new URL(remoteURL).hostname}...`);
try {
const response = await fetch(remoteURL, { cache: "no-store" });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const remoteData = await response.json();

let convertedMappings = [];


if (remoteURL.includes('pagetualRules.json') && typeof remoteData ===
'object' && !Array.isArray(remoteData)) {
for (const domainPattern in remoteData) {
const rule = remoteData[domainPattern];
// Keep disabled rules, let the user enable/disable locally if
needed
// but skip if essential selectors are missing.
if (!rule.pageElement || !rule.nextLink) continue;

convertedMappings.push({
domainPattern: domainPattern,
contentSelector: rule.pageElement,
nextPageSelector: rule.nextLink,
disabled: rule['[Disabled]'] === true // Convert Pagetual
disable flag
// Add other flags here if needed later
});
}
} else if (Array.isArray(remoteData)) {
convertedMappings = remoteData; // Assume correct format
} else {
throw new Error("Fetched data format not recognized (Pagetual
object or Standard array).");
}
if (saveDomainMappings(convertedMappings)) {
showToast(`Workspaceed ${convertedMappings.length} rules. Reload
page to apply.`, 5000);
}
} catch (err) {
console.error(`${SCRIPT_NAME}: Error fetching or processing remote
mappings:`, err);
showToast(`Failed to fetch mappings: ${err.message}`, 6000, true);
}
}

// ------------------------------------------------------
// 6. Domain Matching & Config Retrieval
// ------------------------------------------------------
function matchDomain(hostname, pattern) {
try {
if (hostname === pattern) return true;
if (pattern.startsWith('*.')) {
return hostname.endsWith(pattern.substring(1));
}
if (!pattern.includes('*')) {
return hostname.endsWith('.' + pattern);
}
// Add more complex matching if needed (e.g., regex from pattern?)
} catch(e) {
console.error(`${SCRIPT_NAME}: Error matching domain "${hostname}"
with pattern "${pattern}":`, e);
return false;
}
return false; // Default if no specific match logic applies
}

function getCurrentDomainConfig(hostname) {
const mappings = getDomainMappings();
for (const mapping of mappings) {
if (mapping.domainPattern && matchDomain(hostname,
mapping.domainPattern)) {
const domainIsDisabled = mapping.disabled === true;
return {
...builtInConfig,
contentSelector: mapping.contentSelector ||
builtInConfig.contentSelector,
nextPageSelector: mapping.nextPageSelector || null,
lazyImagesEnabled: typeof mapping.lazyImagesEnabled ===
'boolean' ? mapping.lazyImagesEnabled : settings.lazyImagesEnabled,
autoNextPageEnabled: typeof mapping.autoNextPageEnabled ===
'boolean' ? mapping.autoNextPageEnabled : settings.autoNextPageEnabled,
isDisabled: domainIsDisabled, // Set the final disabled state
matchedPattern: mapping.domainPattern // Store which pattern
matched for info
};
}
}
// No specific match found, use global settings
return {
...builtInConfig,
lazyImagesEnabled: settings.lazyImagesEnabled,
autoNextPageEnabled: settings.autoNextPageEnabled,
isDisabled: false, // Not disabled if no specific rule matched
matchedPattern: null
};
}

// ------------------------------------------------------
// 7. Core Feature Initialization & DOM Ready Logic
// ------------------------------------------------------
const currentHostname = window.location.hostname;
let domainConfig = null;
let intersectionObserverImage = null;
let intersectionObserverNextPage = null;
let imageMutationObserver = null;

// Function to initialize features based on the final config


async function initializeScript() {
// Load settings first thing
settings = GM_getValue(SETTINGS_KEY, defaultSettings);

// --- Global Disable Check ---


if (settings.globallyDisabled) {
console.log(`${SCRIPT_NAME}: Globally disabled via settings.`);
// Optionally show a subtle indicator or do nothing
// createSettingsUI(); // Still create UI to allow re-enabling
return; // Stop further initialization
}

// --- Fetch Mappings if Needed ---


const localMappings = getDomainMappings();
if (settings.autoFetchRemoteMappings && settings.remoteMappingURL &&
(localMappings.length === 0 || settings.remoteMappingURL ===
PAGETUAL_RULES_URL)) {
// console.log(`${SCRIPT_NAME}: Auto-fetching remote mappings...`);
await fetchRemoteDomainMappings(settings.remoteMappingURL); // Wait for
fetch
}

// --- Get Config & Domain Disable Check ---


domainConfig = getCurrentDomainConfig(currentHostname);
// console.log(`${SCRIPT_NAME}: Using config for ${currentHostname}:`,
domainConfig);

if (domainConfig.isDisabled) {
console.log(`${SCRIPT_NAME}: Disabled for domain ${currentHostname} by
matching pattern: ${domainConfig.matchedPattern}`);
createSettingsUI(); // Still create UI
return; // Stop further initialization for this domain
}

// --- Initialize Active Features ---


if (domainConfig.lazyImagesEnabled) {
initLazyImageLoading(document.body); // Initial pass
initMutationObserver(); // Start observing for dynamic images
}
if (domainConfig.autoNextPageEnabled) {
initAutoNextPage();
}

// --- Create Settings UI ---


// Needs to be called AFTER config is determined so UI reflects correct
state
createSettingsUI();
}

// Inject Base Styles Immediately (into Shadow DOM)


injectBaseStyles(); // Definition further down

// Initialize on DOMContentLoaded
if (document.readyState === 'loading') { // Fire early if possible
document.addEventListener('DOMContentLoaded', initializeScript, { once:
true });
} else {
initializeScript(); // Already loaded
}

// ------------------------------------------------------
// 8. Style Injection (CSS for Shadow DOM)
// ------------------------------------------------------
function injectBaseStyles() {
const style = document.createElement('style');
style.textContent = `
/* Reset styles within Shadow DOM to minimize conflicts */
:host { display: block; /* Required for host element */ }
* { box-sizing: border-box; margin: 0; padding: 0; font-family: sans-
serif; }

/* Toast Notification Style */


.asb-toast {
position: fixed; /* Fixed relative to viewport */
bottom: 20px;
right: 20px;
background: rgba(40, 40, 40, 0.9);
color: white;
padding: 10px 18px;
border-radius: 5px;
z-index: 100001; /* Ensure it's above other UI */
opacity: 0;
font-size: 13px;
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
transform: translateY(10px); /* Start slightly lower */
box-shadow: 0 3px 8px rgba(0,0,0,0.3);
max-width: 300px; /* Prevent overly wide toasts */
word-wrap: break-word; /* Wrap long messages */
}
.asb-toast-error {
background: rgba(217, 83, 79, 0.95); /* Red for errors */
color: white;
}

/* Settings Button */
#asbSettingsBtn {
position: fixed; bottom: 10px; right: 10px;
background: rgba(40,40,40,0.75); color: #fff; border: none;
border-radius: 5px; padding: 8px 12px; z-index: 99998;
font-size: 14px; cursor: pointer; box-shadow: 0 2px 5px
rgba(0,0,0,0.2);
transition: background-color 0.2s;
}
#asbSettingsBtn:hover { background: rgba(70,70,70,0.85); }

/* Settings Panel */
#asbSettingsPanel {
display: none; /* Hidden by default */
position: fixed; bottom: 55px; right: 10px;
width: 300px; background: #f4f4f4; border: 1px solid #ccc;
border-radius: 6px; padding: 15px; z-index: 99999;
font-size: 13px; color: #333; box-shadow: 0 4px 12px
rgba(0,0,0,0.2);
max-height: calc(90vh - 70px); /* Limit height */
overflow-y: auto; /* Scroll if content exceeds height */
scrollbar-width: thin; /* Firefox scrollbar */
}
/* Webkit scrollbar styles */
#asbSettingsPanel::-webkit-scrollbar { width: 6px; }
#asbSettingsPanel::-webkit-scrollbar-track { background: #f1f1f1;
border-radius: 3px;}
#asbSettingsPanel::-webkit-scrollbar-thumb { background: #aaa; border-
radius: 3px;}
#asbSettingsPanel::-webkit-scrollbar-thumb:hover { background: #888; }

#asbSettingsPanel h3 { margin: 0 0 15px; font-size: 16px; text-align:


center; color: #111; font-weight: 600; }
#asbSettingsPanel label { display: block; margin-bottom: 10px; font-
weight: bold; }
#asbSettingsPanel .label-normal { font-weight: normal; display: inline-
block; margin-left: 5px;}
#asbSettingsPanel input[type="checkbox"] { margin-right: 6px; vertical-
align: middle; width: 16px; height: 16px; accent-color: #337ab7;}
#asbSettingsPanel input[type="number"],
#asbSettingsPanel input[type="text"] {
margin-left: 5px; padding: 5px 8px; border: 1px solid #ccc;
border-radius: 4px; width: 65px; font-size: 13px;
}
#asbSettingsPanel input[type="text"] { width: calc(100% - 15px);
margin-left: 0; margin-top: 4px; }
#asbSettingsPanel .setting-group { margin-bottom: 15px; padding-bottom:
12px; border-bottom: 1px solid #ddd; }
#asbSettingsPanel .setting-group:last-of-type { border-bottom: none;
margin-bottom: 0; padding-bottom: 0;}
#asbSettingsPanel .button-group { margin-top: 15px; display: flex;
justify-content: flex-end; gap: 8px;}
#asbSettingsPanel button {
padding: 7px 12px; font-size: 12px; cursor: pointer;
border: 1px solid #adadad; border-radius: 4px; background-color:
#e8e8e8; color: #333;
transition: background-color 0.2s, border-color 0.2s;
}
#asbSettingsPanel button:hover { background-color: #dcdcdc; border-
color: #999;}
#asbSettingsPanel button:active { background-color: #d0d0d0;}
#asbSettingsPanel button#asbClosePanel { background: #e8e8e8; color:
#333; border-color: #adadad; }
#asbSettingsPanel button#asbClosePanel:hover { background: #dcdcdc;
border-color: #999;}
#asbSettingsPanel button#asbResetDefaults { background-color: #f0ad4e;
color: white; border-color: #eea236;}
#asbSettingsPanel button#asbResetDefaults:hover { background-color:
#ec971f; border-color: #d58512;}

/* Domain Mapping Modal */


#asbDomainModal {
display: none; /* Set to 'flex' to show */
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6); z-index: 100000; justify-content:
center; align-items: center;
backdrop-filter: blur(2px); /* Optional blur effect */
}
#asbDomainModalContent {
background: #fff; padding: 25px; border-radius: 6px; width: 90%;
max-width: 650px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3); display: flex; flex-
direction: column;
max-height: 85vh; /* Limit modal height */
}
#asbDomainModal h4 { margin: 0 0 15px; text-align: center; font-size:
16px; font-weight: 600;}
#asbDomainModal p { font-size: 12px; color: #555; margin-top: -10px;
margin-bottom: 10px; line-height: 1.4;}
#asbDomainModal code { background: #eee; padding: 1px 4px; border-
radius: 3px; font-size: 11px;}
#asbDomainMappingsTextarea {
width: 100%; height: 350px; font-family: monospace; font-size:
12px; line-height: 1.3;
border: 1px solid #ccc; margin-bottom: 15px; padding: 10px; box-
sizing: border-box;
resize: vertical; /* Allow vertical resize */
}
#asbDomainModalButtons { display: flex; justify-content: flex-end; gap:
10px; }
#asbDomainModal button { padding: 9px 18px; font-size: 13px; border-
radius: 4px; cursor: pointer;}
#asbDomainModal button#asbDomainSave { background-color: #5cb85c;
color: white; border: 1px solid #4cae4c;}
#asbDomainModal button#asbDomainSave:hover { background-color: #449d44;
border-color: #398439;}
#asbDomainModal button#asbDomainCancel { background-color: #f1f1f1;
color: #333; border: 1px solid #ccc;}
#asbDomainModal button#asbDomainCancel:hover { background-color:
#e1e1e1; border-color: #bbb;}

/* Infinite Scroll Loader */


#asbNextPageLoader {
text-align: center; padding: 25px; font-style: italic; color: #888;
display: none; /* Hidden by default */
width: 100%; clear: both; font-size: 14px;
}
`;
shadowRoot.appendChild(style); // Append styles inside shadow DOM
}

// ------------------------------------------------------
// 9. Settings UI Creation & Management (Inside Shadow DOM)
// ------------------------------------------------------
function createSettingsUI() {
// No need to check document.body, shadowRoot always exists here
// Avoid creating UI multiple times by checking for an element inside
shadowRoot
if (shadowRoot.getElementById('asbSettingsPanel')) return;

const settingsBtn = document.createElement('button');


settingsBtn.id = 'asbSettingsBtn';
settingsBtn.textContent = '⚡';
settingsBtn.title = `${SCRIPT_NAME} Settings`;
shadowRoot.appendChild(settingsBtn);

const panel = document.createElement('div');


panel.id = 'asbSettingsPanel';
panel.style.display = 'none'; // Start hidden
panel.innerHTML = `
<h3>${SCRIPT_NAME}</h3>
<div class="setting-group">
<label title="Globally enable or disable all script features">
<input type="checkbox" id="asbGlobalEnableToggle">
<span class="label-normal">Enable Script Globally</span>
</label>
</div>
<div class="setting-group">
<label><input type="checkbox" id="asbLazyToggle"> Enable Lazy Image
Loading</label>
<label><input type="checkbox" id="asbAutoNextToggle"> Enable
Infinite Scroll</label>
<label> Image Margin: <input type="number" id="asbImgMargin"
min="0" step="10"> px </label>
</div>
<div class="setting-group">
<label for="asbRemoteMappingsURL">Remote Mappings URL:</label>
<input type="text" id="asbRemoteMappingsURL" placeholder="URL to
JSON rules...">
<label style="margin-top: 5px;">
<input type="checkbox" id="asbAutoFetchToggle">
<span class="label-normal">Auto-fetch on load</span>
</label>
<div class="button-group" style="justify-content: flex-start;
margin-top: 8px;">
<button id="asbFetchRemote" title="Fetch and replace local
mappings now">Fetch Remote Now</button>
</div>
</div>
<div class="setting-group">
<button id="asbEditDomains">Edit Domain Mappings</button>
</div>
<div class="button-group">
<button id="asbResetDefaults" title="Clear all settings and
mappings, revert to defaults">Reset Defaults</button>
<button id="asbClosePanel">Close</button>
</div>
`;
shadowRoot.appendChild(panel);

const modal = document.createElement('div');


modal.id = 'asbDomainModal';
modal.style.display = 'none';
modal.innerHTML = `
<div id="asbDomainModalContent">
<h4>Edit Domain Mappings (JSON Array)</h4>
<p>
Define rules as a JSON array. Use <code>"disabled": true</code>
to disable for a domain.<br/>
Example: <code>[{"domainPattern": "*.example.com",
"contentSelector": "#main", "nextPageSelector": ".next", "disabled":
false}, ...]</code>
</p>
<textarea id="asbDomainMappingsTextarea"></textarea>
<div id="asbDomainModalButtons">
<button id="asbDomainCancel">Cancel</button>
<button id="asbDomainSave">Save</button>
</div>
</div>
`;
shadowRoot.appendChild(modal);

// --- Initialize Control States from `settings` ---


// Use try/catch as elements might not exist if UI creation fails partially
(unlikely)
try {
// Note: Global enable toggle is inverse of globallyDisabled setting
shadowRoot.getElementById('asbGlobalEnableToggle').checked = !
settings.globallyDisabled;
shadowRoot.getElementById('asbLazyToggle').checked =
settings.lazyImagesEnabled;
shadowRoot.getElementById('asbAutoNextToggle').checked =
settings.autoNextPageEnabled;
shadowRoot.getElementById('asbImgMargin').value = settings.imageMargin;
shadowRoot.getElementById('asbRemoteMappingsURL').value =
settings.remoteMappingURL || '';
shadowRoot.getElementById('asbAutoFetchToggle').checked =
settings.autoFetchRemoteMappings;
} catch (e) {
console.error(`${SCRIPT_NAME}: Error initializing UI control states:`,
e);
}

// --- Event Listeners ---


settingsBtn.addEventListener('click', () => { panel.style.display =
panel.style.display === 'block' ? 'none' : 'block'; });
shadowRoot.getElementById('asbClosePanel').addEventListener('click', () =>
{ panel.style.display = 'none'; });

panel.addEventListener('change', (event) => {


// Get all values on any change within the panel
const newSettings = {
globallyDisabled: !
shadowRoot.getElementById('asbGlobalEnableToggle')?.checked, // Inverse logic
lazyImagesEnabled:
shadowRoot.getElementById('asbLazyToggle')?.checked,
autoNextPageEnabled:
shadowRoot.getElementById('asbAutoNextToggle')?.checked,
imageMargin:
parseInt(shadowRoot.getElementById('asbImgMargin')?.value, 10) ||
defaultSettings.imageMargin,
remoteMappingURL:
shadowRoot.getElementById('asbRemoteMappingsURL')?.value.trim(),
autoFetchRemoteMappings:
shadowRoot.getElementById('asbAutoFetchToggle')?.checked
};
saveSettings(newSettings);
// Provide feedback, maybe subtle, maybe only on significant changes
// showToast("Settings updated", 1500); // Can be too noisy
});

shadowRoot.getElementById('asbFetchRemote').addEventListener('click', () =>
{
const url =
shadowRoot.getElementById('asbRemoteMappingsURL').value.trim();
fetchRemoteDomainMappings(url); // Async function call
});

shadowRoot.getElementById('asbEditDomains').addEventListener('click', () =>
{
const currentMappings = JSON.stringify(getDomainMappings(), null, 2);
shadowRoot.getElementById('asbDomainMappingsTextarea').value =
currentMappings;
modal.style.display = 'flex'; // Use flex for centering via CSS
});

shadowRoot.getElementById('asbDomainCancel').addEventListener('click', ()
=> { modal.style.display = 'none'; });

shadowRoot.getElementById('asbDomainSave').addEventListener('click', () =>
{
const newMappingsStr =
shadowRoot.getElementById('asbDomainMappingsTextarea').value;
try {
const newMappings = JSON.parse(newMappingsStr);
if (saveDomainMappings(newMappings)) { // save checks array type
showToast("Domain mappings saved. Reload page to apply.");
modal.style.display = 'none';
}
} catch (e) {
console.error(`${SCRIPT_NAME}: Invalid JSON in domain mappings:`,
e);
showToast(`Invalid JSON format: ${e.message}`, 5000, true);
}
});

// Reset Defaults Button


shadowRoot.getElementById('asbResetDefaults').addEventListener('click', ()
=> {
if (confirm('Are you sure you want to reset all settings and clear
custom domain mappings?')) {
resetSettingsAndMappings();
// Re-initialize UI state after reset
shadowRoot.getElementById('asbGlobalEnableToggle').checked = !
settings.globallyDisabled;
shadowRoot.getElementById('asbLazyToggle').checked =
settings.lazyImagesEnabled;
shadowRoot.getElementById('asbAutoNextToggle').checked =
settings.autoNextPageEnabled;
shadowRoot.getElementById('asbImgMargin').value =
settings.imageMargin;
shadowRoot.getElementById('asbRemoteMappingsURL').value =
settings.remoteMappingURL || '';
shadowRoot.getElementById('asbAutoFetchToggle').checked =
settings.autoFetchRemoteMappings;
showToast('Settings reset to defaults. Reload page.');
}
});
}

// --- Tampermonkey Menu Command ---


if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand(`⚡ ${SCRIPT_NAME} Settings`, () => {
let panel = shadowRoot.getElementById('asbSettingsPanel');
if (!panel) {
createSettingsUI(); // Create if not exists (e.g., script re-
enabled)
panel = shadowRoot.getElementById('asbSettingsPanel');
}
if (panel) { // Toggle visibility
panel.style.display = panel.style.display === 'block' ? 'none' :
'block';
}
});
}

// ------------------------------------------------------
// 10. Lazy Loading Images (using data-src)
// ------------------------------------------------------
function initLazyImageLoading(containerElement) {
if (!domainConfig || !domainConfig.lazyImagesEnabled) return; // Check
feature enabled

// Use :where() for zero specificity to avoid conflicts if page uses .asb-
loaded
const images = containerElement.querySelectorAll('img[data-
src]:where(:not(.asb-loaded))');
if (images.length === 0) return;

if ('IntersectionObserver' in window) {
if (!intersectionObserverImage) { // Create observer only once
intersectionObserverImage = new IntersectionObserver((entries,
observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const dataSrc = img.getAttribute('data-src');
img.src = dataSrc; // Set src to trigger load
img.onload = () => img.classList.add('asb-loaded');
img.onerror = () => {
img.classList.add('asb-error');
console.warn(`${SCRIPT_NAME}: Failed to load
image:`, dataSrc);
};
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
}, {
rootMargin: `${settings.imageMargin}px 0px`,
threshold: 0.01 // Load even if only slightly visible
});
}
images.forEach(img => intersectionObserverImage.observe(img));
} else { // Fallback
images.forEach(img => {
img.src = img.getAttribute('data-src');
img.onload = () => img.classList.add('asb-loaded');
img.removeAttribute('data-src');
});
}
}

// ------------------------------------------------------
// 11. Mutation Observer for Dynamic Images
// ------------------------------------------------------
function initMutationObserver() {
// Check dependencies and ensure not already running
if (!window.MutationObserver || !domainConfig || !
domainConfig.lazyImagesEnabled || imageMutationObserver) {
return;
}

const targetNode = document.body; // Observe the whole body


if (!targetNode) return; // Body must exist

const config = { childList: true, subtree: true };

const callback = function(mutationsList, observer) {


for(const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length >
0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) { // Check if it's an element node
// Check the node itself if it's an image
if (node.matches && node.matches('img[data-
src]:where(:not(.asb-loaded))')) {
if (intersectionObserverImage)
intersectionObserverImage.observe(node);
}
// Check descendants if the added node is a container
else if (node.querySelector) { // Ensure querySelector
exists
const newImages = node.querySelectorAll('img[data-
src]:where(:not(.asb-loaded))');
if (newImages.length > 0) {
// console.log(`${SCRIPT_NAME}
MutationObserver: Found ${newImages.length} dynamic images.`);
newImages.forEach(img => {
if (intersectionObserverImage)
intersectionObserverImage.observe(img);
});
}
}
}
});
}
}
};

imageMutationObserver = new MutationObserver(callback);


imageMutationObserver.observe(targetNode, config);
// console.log(`${SCRIPT_NAME}: MutationObserver initialized for dynamic
images.`);

// Consider disconnecting the observer if the script is disabled later? Or


on page unload?
// window.addEventListener('beforeunload', () => {
// if(imageMutationObserver) imageMutationObserver.disconnect();
// });
}

// ------------------------------------------------------
// 12. Infinite Scroll Implementation
// ------------------------------------------------------
let isLoadingNextPage = false;
let nextPageUrl = null;
let nextPageTrigger = null;
let contentContainer = null;
let nextPageLoaderIndicator = null;

function findNextPageLink(doc = document) { // Allow searching specific doc


if (!domainConfig) return null;
// 1. Try the domain-specific selector first
if (domainConfig.nextPageSelector) {
try {
const specificLink =
doc.querySelector(domainConfig.nextPageSelector);
// Check for valid href and not just '#' or javascript:
if (specificLink && specificLink.href && !
specificLink.href.startsWith('javascript:') && !specificLink.href.endsWith('#')) {
return specificLink.href;
}
} catch (e) { console.warn(`${SCRIPT_NAME}: Error querying
nextPageSelector "${domainConfig.nextPageSelector}":`, e); }
}
// Fallback logic
const relNextLink = doc.querySelector('link[rel="next"]');
if (relNextLink && relNextLink.href) return relNextLink.href;

const anchors = doc.querySelectorAll('a[href]');


const commonPatterns = [/next/i, /older/i, />>/, /»/, /suivant/i,
/siguiente/i, /load more/i, /page.*[+»>]/i];
let potentialLinks = [];
for (const a of anchors) {
// Basic filtering
if (!a.href || a.href === window.location.href ||
a.href.startsWith('javascript:') || a.href.endsWith('#')) continue;
// Check common patterns in text, title, or class
for (const pattern of commonPatterns) {
if (pattern.test(a.textContent || '') || pattern.test(a.title ||
'') || pattern.test(a.className || '')) {
potentialLinks.push(a);
break; // Found pattern for this link, move to next link
}
}
}
// Maybe add simple heuristics like choosing the link furthest down the
page?
// For now, just return the first likely candidate found by pattern
if(potentialLinks.length > 0) return potentialLinks[0].href;

return null;
}

async function loadNextPageContent() {


if (isLoadingNextPage || !nextPageUrl || !contentContainer || !
domainConfig) return;

isLoadingNextPage = true;
if (nextPageLoaderIndicator) nextPageLoaderIndicator.style.display =
'block';

try {
const response = await fetch(nextPageUrl);
if (!response.ok) throw new Error(`HTTP error! Status: $
{response.status}`);
const htmlText = await response.text();
const parser = new DOMParser();
const nextPageDoc = parser.parseFromString(htmlText, 'text/html');

let newContentFragment = null;


try {
newContentFragment =
nextPageDoc.querySelector(domainConfig.contentSelector);
} catch(e) { throw new Error(`Error finding content selector "$
{domainConfig.contentSelector}" in fetched page: ${e.message}`); }

if (newContentFragment) {
// Append children safely
while (newContentFragment.firstChild) {
contentContainer.appendChild(newContentFragment.firstChild);
}

// Re-initialize lazy loading only if enabled


if (domainConfig.lazyImagesEnabled) {
initLazyImageLoading(contentContainer); // Scan only needed if
MutationObserver isn't active/sufficient? Check this.
}

// Find the link for the *subsequent* page load from the *fetched*
document
nextPageUrl = findNextPageLink(nextPageDoc); // Use helper with doc
context

if (!nextPageUrl) {
if (nextPageTrigger && intersectionObserverNextPage)
intersectionObserverNextPage.unobserve(nextPageTrigger);
if (nextPageLoaderIndicator)
nextPageLoaderIndicator.textContent = "End of content.";
}
} else {
throw new Error(`Content selector "$
{domainConfig.contentSelector}" not found in fetched page.`);
}
} catch (error) {
console.error(`${SCRIPT_NAME}: Error loading/processing next page:`,
error);
if (nextPageLoaderIndicator) nextPageLoaderIndicator.textContent =
`Error: ${error.message.substring(0, 100)}`; // Show error briefly
if (nextPageTrigger && intersectionObserverNextPage)
intersectionObserverNextPage.unobserve(nextPageTrigger);
nextPageUrl = null; // Stop trying after error
} finally {
isLoadingNextPage = false;
// Keep loader visible only if it shows end/error message
if (nextPageLoaderIndicator && !(nextPageLoaderIndicator.textContent
=== "End of content." || nextPageLoaderIndicator.textContent.startsWith("Error")))
{
nextPageLoaderIndicator.style.display = 'none';
}
}
}

function initAutoNextPage() {
if (!domainConfig || !domainConfig.autoNextPageEnabled) return; // Check
feature enabled

try {
contentContainer =
document.querySelector(domainConfig.contentSelector);
} catch (e) { console.error(`${SCRIPT_NAME}: Error querying content
container "${domainConfig.contentSelector}":`, e); }

if (!contentContainer) {
console.warn(`${SCRIPT_NAME}: Infinite scroll disabled - content
container not found:`, domainConfig.contentSelector);
return;
}

nextPageUrl = findNextPageLink(document); // Find initial link


if (!nextPageUrl) {
// console.log(`${SCRIPT_NAME}: Infinite scroll disabled - no initial
next page link found.`);
return;
}

// Create trigger and loader elements if they don't exist


if (!document.getElementById('asbNextPageTrigger')) { // Check real DOM,
not Shadow DOM
nextPageTrigger = document.createElement('div');
nextPageTrigger.id = 'asbNextPageTrigger';
nextPageTrigger.style.cssText = 'width:100%; height:100px; margin-top:
50px; clear:both; visibility: hidden;'; // Make it invisible

nextPageLoaderIndicator = document.createElement('div');
nextPageLoaderIndicator.id = 'asbNextPageLoader';
nextPageLoaderIndicator.textContent = 'Loading next page...';
// Inject styles for loader via JS (as it's outside Shadow DOM) or use
a class defined globally?
// Simpler to style directly for now.
Object.assign(nextPageLoaderIndicator.style, {
textAlign: 'center', padding: '25px', fontStyle: 'italic', color:
'#888',
display: 'none', width: '100%', clear: 'both', fontSize: '14px'
});

try { // --- Add try/catch around insertion ---


contentContainer.parentNode.insertBefore(nextPageTrigger,
contentContainer.nextSibling);
nextPageTrigger.parentNode.insertBefore(nextPageLoaderIndicator,
nextPageTrigger.nextSibling);
} catch (e) {
console.error(`${SCRIPT_NAME} Error: Failed to append infinite
scroll trigger/loader. Disabling infinite scroll.`, e);
return; // Stop if elements can't be added
}

} else { // Reuse existing if needed


nextPageTrigger = document.getElementById('asbNextPageTrigger');
nextPageLoaderIndicator =
document.getElementById('asbNextPageLoader');
if(nextPageLoaderIndicator) nextPageLoaderIndicator.style.display =
'none';
}

if ('IntersectionObserver' in window) {
if (intersectionObserverNextPage)
intersectionObserverNextPage.disconnect(); // Ensure clean state

intersectionObserverNextPage = new IntersectionObserver(entries => {


entries.forEach(entry => {
if (entry.isIntersecting && !isLoadingNextPage && nextPageUrl)
{
loadNextPageContent(); // Async function
}
});
}, {
rootMargin: '500px 0px', // Increased margin further
threshold: 0.01
});

intersectionObserverNextPage.observe(nextPageTrigger);
} else {
console.warn(`${SCRIPT_NAME}: IntersectionObserver not supported,
infinite scroll disabled.`);
}
}

})(); // End of UserScript wrapper

You might also like