diff --git a/apollocafe-image-upgrader.user.js b/apollocafe-image-upgrader.user.js
new file mode 100644
index 0000000..4e13420
--- /dev/null
+++ b/apollocafe-image-upgrader.user.js
@@ -0,0 +1,33 @@
+// ==UserScript==
+// @name Apollo Cafe Image Upgrader
+// @namespace https://apollo.cafe/
+// @version 1.0
+// @description Replace all imagedelivery.net URLs ending in /2x with /4x on apollo.cafe
+// @match https://apollo.cafe/*
+// @match http://apollo.cafe/*
+// @icon https://apollo.cafe/favicon.ico
+// @grant none
+// ==/UserScript==
+
+(function() {
+ 'use strict';
+
+ // Function to replace /2x -> /4x in all tags
+ function upgradeImages() {
+ document.querySelectorAll('img[src*="imagedelivery.net"]').forEach(img => {
+ img.src = img.src.replace(/\/2x$/, '/4x');
+ });
+
+ // Also replace inline HTML / CSS background URLs if needed
+ document.querySelectorAll('[style*="imagedelivery.net"]').forEach(el => {
+ el.style.backgroundImage = el.style.backgroundImage.replace(/\/2x(['"]?\)?$)/, '/4x$1');
+ });
+ }
+
+ // Run once at page load
+ upgradeImages();
+
+ // Run again whenever DOM changes (for infinite scroll, SPA, etc.)
+ const observer = new MutationObserver(() => upgradeImages());
+ observer.observe(document.body, { childList: true, subtree: true });
+})();
diff --git a/expand-tco-links.user.js b/expand-tco-links.user.js
new file mode 100644
index 0000000..4199450
--- /dev/null
+++ b/expand-tco-links.user.js
@@ -0,0 +1,135 @@
+// ==UserScript==
+// @name Expand t.co links (live replace, Location header fixed)
+// @namespace ergosteur
+// @version 0.7
+// @description Expand t.co shortlinks into final URLs by parsing Location header
+// @match https://twitter.com/*
+// @match https://x.com/*
+// @grant GM_registerMenuCommand
+// @grant GM_xmlhttpRequest
+// ==/UserScript==
+
+(function() {
+ 'use strict';
+
+ function showTextareaDialog(title, callback) {
+ const overlay = document.createElement("div");
+ overlay.style.position = "fixed";
+ overlay.style.top = 0;
+ overlay.style.left = 0;
+ overlay.style.width = "100%";
+ overlay.style.height = "100%";
+ overlay.style.background = "rgba(0,0,0,0.7)";
+ overlay.style.zIndex = 999999;
+
+ const box = document.createElement("div");
+ box.style.position = "absolute";
+ box.style.top = "50%";
+ box.style.left = "50%";
+ box.style.transform = "translate(-50%, -50%)";
+ box.style.background = "#fff";
+ box.style.padding = "20px";
+ box.style.borderRadius = "8px";
+ box.style.maxWidth = "600px";
+ box.style.width = "80%";
+ box.style.display = "flex";
+ box.style.flexDirection = "column";
+
+ const label = document.createElement("div");
+ label.textContent = title;
+ label.style.marginBottom = "8px";
+
+ const textarea = document.createElement("textarea");
+ textarea.style.width = "100%";
+ textarea.style.height = "300px";
+ textarea.style.whiteSpace = "pre";
+
+ const controls = document.createElement("div");
+ controls.style.marginTop = "8px";
+ controls.style.display = "flex";
+ controls.style.justifyContent = "space-between";
+ controls.style.alignItems = "center";
+
+ const button = document.createElement("button");
+ button.textContent = "Expand";
+
+ const status = document.createElement("div");
+ status.textContent = "Ready.";
+
+ controls.appendChild(button);
+ controls.appendChild(status);
+
+ box.appendChild(label);
+ box.appendChild(textarea);
+ box.appendChild(controls);
+ overlay.appendChild(box);
+ document.body.appendChild(overlay);
+
+ button.onclick = () => {
+ const val = textarea.value.trim();
+ if (!val) {
+ overlay.remove();
+ return;
+ }
+ callback(textarea, status);
+ };
+ }
+
+ function expandOne(link) {
+ return new Promise((resolve) => {
+ GM_xmlhttpRequest({
+ method: "GET", // GET is safer than HEAD for t.co
+ url: link,
+ redirect: "manual", // we want the Location header
+ onload: (resp) => {
+ let out = link;
+
+ // responseHeaders is a string in Tampermonkey
+ const headers = resp.responseHeaders.split(/\r?\n/);
+ const locLine = headers.find(h => /^location:/i.test(h));
+ if (locLine) {
+ out = locLine.split(/:\s*/i)[1].trim();
+ }
+
+ resolve(out);
+ },
+ onerror: () => resolve(link)
+ });
+ });
+ }
+
+ async function expandLinks() {
+ showTextareaDialog("Paste t.co links (one per line):", async (textarea, status) => {
+ let lines = textarea.value.split(/\r?\n/);
+ const total = lines.length;
+
+ for (let i = 0; i < total; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ status.textContent = `Expanding ${i + 1}/${total}...`;
+ const full = await expandOne(line);
+
+ lines[i] = full;
+ textarea.value = lines.join("\n");
+
+ textarea.scrollTop = textarea.scrollHeight; // auto-scroll
+ }
+
+ status.textContent = `Done! Expanded ${total} links.`;
+
+ const blob = new Blob([lines.join("\n")], {type: "text/plain"});
+ const url = URL.createObjectURL(blob);
+
+ const dl = document.createElement("a");
+ dl.href = url;
+ dl.download = "expanded_links.txt";
+ dl.textContent = "⬇ Download results";
+ dl.style.marginLeft = "10px";
+
+ status.appendChild(dl);
+ });
+ }
+
+ GM_registerMenuCommand("Expand t.co Links", expandLinks);
+})();
\ No newline at end of file
diff --git a/lazyload-link-collector.user.js b/lazyload-link-collector.user.js
new file mode 100644
index 0000000..8d660df
--- /dev/null
+++ b/lazyload-link-collector.user.js
@@ -0,0 +1,345 @@
+// ==UserScript==
+// @name Extract Links (Universal, Configurable Patterns)
+// @namespace ergosteur
+// @version 2.9
+// @description Universal lazy-scroll link extractor with config, HUD, stop button, and Tampermonkey menu
+// @match *://*/*
+// @grant GM_registerMenuCommand
+// ==/UserScript==
+
+(function () {
+ 'use strict';
+
+ const found = new Set();
+
+ // 🔧 Defaults + persisted settings
+ let hrefPattern = localStorage.hrefPattern || "https://t.co/";
+ let textPattern = localStorage.textPattern || "drive.google.com";
+ let useHrefPattern = localStorage.useHrefPattern !== "false"; // default true
+ let useTextPattern = localStorage.useTextPattern !== "false"; // default true
+ let maxScrolls = parseInt(localStorage.maxScrolls || "0", 10); // 0 = unlimited
+ let autoShowButton = localStorage.autoShowButton === "true"; // default false
+
+ // Scrolling behavior
+ const STEP_FRACTION = 0.9;
+ const SCROLL_DELAY_MS = 700;
+ const SETTLE_DELAY_MS = 800;
+ const STALL_LIMIT = 8;
+ const HARD_CAP = 2000;
+
+ // Progress HUD
+ let progressEl = null;
+ let stopFlag = false;
+
+ function showProgress() {
+ if (!progressEl) {
+ progressEl = document.createElement("div");
+ Object.assign(progressEl.style, {
+ position: "fixed",
+ top: "20px",
+ left: "20px",
+ zIndex: "1000000",
+ background: "rgba(0,0,0,0.7)",
+ color: "white",
+ padding: "6px 10px",
+ borderRadius: "4px",
+ fontSize: "13px",
+ fontFamily: "monospace",
+ display: "flex",
+ alignItems: "center",
+ gap: "8px"
+ });
+
+ const label = document.createElement("span");
+ label.id = "progress-label";
+ progressEl.appendChild(label);
+
+ const stopBtn = document.createElement("button");
+ stopBtn.innerText = "⏹ Stop";
+ Object.assign(stopBtn.style, {
+ background: "#d9534f",
+ color: "white",
+ border: "none",
+ padding: "2px 6px",
+ borderRadius: "3px",
+ cursor: "pointer",
+ fontSize: "12px"
+ });
+ stopBtn.onclick = () => { stopFlag = true; };
+ progressEl.appendChild(stopBtn);
+
+ document.body.appendChild(progressEl);
+ }
+ }
+
+ function updateProgress(rounds) {
+ const spinner = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"][rounds % 10];
+ const label = document.getElementById("progress-label");
+ if (label) {
+ label.textContent = `${spinner} Rounds: ${rounds} | Links: ${found.size}`;
+ }
+ }
+
+ function hideProgress() {
+ if (progressEl) { progressEl.remove(); progressEl = null; }
+ }
+
+ function extractLinks() {
+ document.querySelectorAll("a").forEach(a => {
+ const href = a.href || "";
+ const text = a.textContent || "";
+ const hrefOk = !useHrefPattern || (hrefPattern && href.includes(hrefPattern));
+ const textOk = !useTextPattern || (textPattern && text.includes(textPattern));
+ if (hrefOk && textOk && href) {
+ if (!found.has(href)) {
+ found.add(href);
+ }
+ }
+ });
+ }
+
+ async function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
+
+ async function scrollToTopAndSettle() {
+ const html = document.documentElement;
+ const prev = html.style.scrollBehavior;
+ html.style.scrollBehavior = "auto";
+ window.scrollTo(0, 0);
+ html.style.scrollBehavior = prev || "";
+ const t0 = performance.now();
+ while (window.scrollY !== 0 && performance.now() - t0 < 1500) {
+ await wait(50);
+ }
+ await wait(SETTLE_DELAY_MS);
+ }
+
+ async function autoScrollAndExtract() {
+ found.clear();
+ stopFlag = false;
+
+ await scrollToTopAndSettle();
+ showProgress();
+
+ const scroller = document.scrollingElement || document.documentElement;
+ let lastHeight = scroller.scrollHeight;
+ let lastFound = 0;
+ let rounds = 0;
+ let stall = 0;
+
+ while (!stopFlag && rounds < HARD_CAP) {
+ rounds++;
+ const step = Math.max(64, Math.floor(window.innerHeight * STEP_FRACTION));
+ window.scrollBy(0, step);
+ await wait(SCROLL_DELAY_MS);
+
+ extractLinks();
+ updateProgress(rounds);
+
+ const newHeight = scroller.scrollHeight;
+ const atBottom = (scroller.scrollTop + window.innerHeight + 2) >= newHeight;
+ const heightGrew = newHeight > lastHeight + 2;
+ const foundGrew = found.size > lastFound;
+
+ if (heightGrew || foundGrew || !atBottom) stall = 0;
+ else stall++;
+
+ lastHeight = newHeight;
+ lastFound = found.size;
+
+ if ((maxScrolls > 0 && rounds >= maxScrolls) || (atBottom && stall >= STALL_LIMIT)) {
+ break;
+ }
+ }
+
+ hideProgress();
+ showOverlay([...found], false);
+ }
+
+ function showOverlay(links, configOnly = false) {
+ const old = document.getElementById("extract-box");
+ if (old) old.remove();
+
+ const container = document.createElement("div");
+ container.id = "extract-box";
+ Object.assign(container.style, {
+ position: "fixed",
+ top: "60px",
+ right: "20px",
+ width: "480px",
+ zIndex: "999999",
+ background: "white",
+ border: "2px solid black",
+ padding: "10px",
+ fontSize: "12px"
+ });
+
+ // Href config
+ const hrefRow = document.createElement("div");
+ const hrefChk = document.createElement("input");
+ hrefChk.type = "checkbox";
+ hrefChk.checked = useHrefPattern;
+ hrefChk.onchange = () => {
+ useHrefPattern = hrefChk.checked;
+ localStorage.useHrefPattern = useHrefPattern;
+ };
+ const hrefLbl = document.createElement("label");
+ hrefLbl.innerText = " Href pattern:";
+ const hrefInp = document.createElement("input");
+ hrefInp.type = "text";
+ hrefInp.value = hrefPattern;
+ Object.assign(hrefInp.style, { width: "70%", marginLeft: "6px" });
+ hrefInp.onchange = () => {
+ hrefPattern = hrefInp.value.trim();
+ localStorage.hrefPattern = hrefPattern;
+ };
+ hrefRow.append(hrefChk, hrefLbl, hrefInp);
+
+ // Text config
+ const textRow = document.createElement("div");
+ const textChk = document.createElement("input");
+ textChk.type = "checkbox";
+ textChk.checked = useTextPattern;
+ textChk.onchange = () => {
+ useTextPattern = textChk.checked;
+ localStorage.useTextPattern = useTextPattern;
+ };
+ const textLbl = document.createElement("label");
+ textLbl.innerText = " Text pattern:";
+ const textInp = document.createElement("input");
+ textInp.type = "text";
+ textInp.value = textPattern;
+ Object.assign(textInp.style, { width: "70%", marginLeft: "6px" });
+ textInp.onchange = () => {
+ textPattern = textInp.value.trim();
+ localStorage.textPattern = textPattern;
+ };
+ textRow.append(textChk, textLbl, textInp);
+
+ // Max scroll config
+ const maxRow = document.createElement("div");
+ const maxLbl = document.createElement("label");
+ maxLbl.innerText = " Max scrolls (0 = unlimited):";
+ const maxInp = document.createElement("input");
+ maxInp.type = "number";
+ maxInp.min = "0";
+ maxInp.value = maxScrolls;
+ Object.assign(maxInp.style, { width: "60px", marginLeft: "6px" });
+ maxInp.onchange = () => {
+ maxScrolls = parseInt(maxInp.value, 10) || 0;
+ localStorage.maxScrolls = maxScrolls;
+ };
+ maxRow.append(maxLbl, maxInp);
+
+ // Auto show button config
+ const autoRow = document.createElement("div");
+ const autoChk = document.createElement("input");
+ autoChk.type = "checkbox";
+ autoChk.checked = autoShowButton;
+ autoChk.onchange = () => {
+ autoShowButton = autoChk.checked;
+ localStorage.autoShowButton = autoShowButton;
+ };
+ const autoLbl = document.createElement("label");
+ autoLbl.innerText = " Show floating button automatically";
+ autoRow.append(autoChk, autoLbl);
+
+ container.append(hrefRow, textRow, maxRow, autoRow);
+
+ if (!useHrefPattern && !useTextPattern) {
+ const warn = document.createElement("div");
+ warn.textContent = "⚠ Warning: Both filters are off. All links will be collected.";
+ Object.assign(warn.style, { color: "red", margin: "6px 0" });
+ container.appendChild(warn);
+ }
+
+ if (!configOnly) {
+ const box = document.createElement("textarea");
+ box.value = links.join("\n");
+ Object.assign(box.style, { width: "100%", height: "200px", margin: "8px 0" });
+ container.appendChild(box);
+
+ const dl = document.createElement("button");
+ dl.innerText = "⬇ Download .txt";
+ Object.assign(dl.style, {
+ background: "#1da1f2", color: "white", border: "none",
+ padding: "6px 10px", borderRadius: "4px", cursor: "pointer", marginRight: "8px"
+ });
+ dl.onclick = () => {
+ const blob = new Blob([links.join("\n")], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url; a.download = "extracted_links.txt";
+ document.body.appendChild(a); a.click(); a.remove();
+ URL.revokeObjectURL(url);
+ };
+ container.appendChild(dl);
+ }
+
+ // Start + Close
+ const start = document.createElement("button");
+ start.innerText = "▶ Start";
+ Object.assign(start.style, {
+ background: "#1da1f2", color: "white", border: "none",
+ padding: "6px 10px", borderRadius: "4px", cursor: "pointer", marginRight: "8px"
+ });
+ start.onclick = () => { container.remove(); autoScrollAndExtract(); };
+
+ const close = document.createElement("button");
+ close.innerText = "❌ Close";
+ Object.assign(close.style, {
+ background: "#aaa", color: "white", border: "none",
+ padding: "6px 10px", borderRadius: "4px", cursor: "pointer"
+ });
+ close.onclick = () => container.remove();
+
+ container.append(start, close);
+ document.body.appendChild(container);
+
+ if (!configOnly) {
+ alert(`✅ Extracted ${links.length} links (href: ${useHrefPattern ? hrefPattern : "ANY"}, text: ${useTextPattern ? textPattern : "ANY"}).`);
+ }
+ }
+
+ function addSplitButton() {
+ if (document.getElementById("extract-btn")) return;
+
+ const wrap = document.createElement("div");
+ wrap.id = "extract-btn";
+ Object.assign(wrap.style, {
+ position: "fixed", top: "20px", right: "20px", zIndex: "999999",
+ display: "flex", borderRadius: "4px", overflow: "hidden",
+ boxShadow: "0 2px 4px rgba(0,0,0,0.2)"
+ });
+
+ const mainBtn = document.createElement("button");
+ mainBtn.innerText = "Extract Links";
+ Object.assign(mainBtn.style, {
+ background: "#1da1f2", color: "white", border: "none",
+ padding: "8px 12px", cursor: "pointer", fontSize: "14px", flex: "1"
+ });
+ mainBtn.onclick = autoScrollAndExtract;
+
+ const cfgBtn = document.createElement("button");
+ cfgBtn.innerText = "⛭";
+ Object.assign(cfgBtn.style, {
+ background: "#0d8ddb", color: "white", border: "none",
+ padding: "8px 10px", cursor: "pointer", fontSize: "14px", width: "40px"
+ });
+ cfgBtn.onclick = () => showOverlay([], true);
+
+ wrap.append(mainBtn, cfgBtn);
+ document.body.appendChild(wrap);
+ }
+
+ // Tampermonkey menu items
+ if (typeof GM_registerMenuCommand !== "undefined") {
+ GM_registerMenuCommand("Show Button", addSplitButton);
+ GM_registerMenuCommand("Extract Links", autoScrollAndExtract);
+ GM_registerMenuCommand("Config", () => showOverlay([], true));
+ }
+
+ // Auto-show only if enabled
+ if (autoShowButton) {
+ setInterval(addSplitButton, 2000);
+ }
+})();
\ No newline at end of file
diff --git a/luxweather-tweaks.user.js b/luxweather-tweaks.user.js
new file mode 100644
index 0000000..1a14005
--- /dev/null
+++ b/luxweather-tweaks.user.js
@@ -0,0 +1,177 @@
+// ==UserScript==
+// @name LuxWeather Frame & Scale Toggle (Settings Integration)
+// @namespace http://tampermonkey.net/
+// @version 1.8
+// @description Toggle CRT frame and scaling (Off, Default, 4:3, 16:9, Stretch) via settings panel with localStorage + keyboard shortcuts
+// @match https://luxweather.com/*
+// @run-at document-idle
+// ==/UserScript==
+
+(function () {
+ const LS_KEY_FRAME = 'luxweather-frame-on';
+ const LS_KEY_SCALE = 'luxweather-scale-mode';
+
+ let frameOn = localStorage.getItem(LS_KEY_FRAME) !== 'false';
+ let scaleMode = parseInt(localStorage.getItem(LS_KEY_SCALE) || '0');
+
+ const style = document.createElement('style');
+ style.innerHTML = `
+ .no-frame .tv--kitty::after,
+ .no-frame .tv--regular::after,
+ .no-frame .tv--apple::after {
+ background-image: none !important;
+ }
+
+ .scale-default .tv {
+ --width: 360;
+ --height: 270;
+ }
+ .scale-default .tv--kitty .tv-screen,
+ .scale-default .tv--regular .tv-screen,
+ .scale-default .tv--apple .tv-screen {
+ top: 50% !important;
+ left: 50% !important;
+ transform: translate(-50%, -50%) !important;
+ width: calc((360 / var(--width)) * 100%);
+ height: calc((270 / var(--height)) * 100%);
+ }
+
+ .scale-fit .tv,
+ .scale-stretch .tv {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ width: 100vw !important;
+ height: 100vh !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: none !important;
+ }
+
+ .scale-fit .tv-screen,
+ .scale-stretch .tv-screen {
+ position: relative !important;
+ background: none !important;
+ top: unset !important;
+ left: unset !important;
+ transform: none !important;
+ }
+
+ .scale-fit .tv-screen {
+ width: 100vw;
+ height: auto;
+ }
+
+ .scale-4-3 .tv-screen {
+ aspect-ratio: 4 / 3;
+ max-height: 100vh;
+ max-width: calc(100vh * (4 / 3));
+ }
+
+ .scale-16-9 .tv-screen {
+ aspect-ratio: 16 / 9;
+ max-height: 100vh;
+ max-width: calc(100vh * (16 / 9));
+ }
+
+ .scale-stretch .tv-screen {
+ width: 100vw !important;
+ height: 100vh !important;
+ object-fit: fill !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ function applyState() {
+ document.body.classList.toggle('no-frame', !frameOn);
+
+ document.body.classList.remove(
+ 'scale-default',
+ 'scale-fit',
+ 'scale-4-3',
+ 'scale-16-9',
+ 'scale-stretch'
+ );
+
+ switch (scaleMode) {
+ case 1:
+ document.body.classList.add('scale-default'); break;
+ case 2:
+ document.body.classList.add('scale-fit', 'scale-4-3'); break;
+ case 3:
+ document.body.classList.add('scale-fit', 'scale-16-9'); break;
+ case 4:
+ document.body.classList.add('scale-stretch'); break;
+ }
+
+ localStorage.setItem(LS_KEY_FRAME, frameOn);
+ localStorage.setItem(LS_KEY_SCALE, scaleMode);
+ }
+
+ function injectSettingsToggles() {
+ const settings = document.getElementById('settings_menu');
+ if (!settings || settings.querySelector('#toggle_frame')) return;
+
+ const group = document.createElement('div');
+ group.className = 'settings_group';
+ group.innerHTML = `
+