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

Android Speed Boost UserScript Guide

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)
60 views17 pages

Android Speed Boost UserScript Guide

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

// ==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