Move userscripts into scripts/ subfolder
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// ==UserScript==
|
||||
// @name Auto Archive Redirect
|
||||
// @namespace Evan Reichard
|
||||
// @version 0.0.3
|
||||
// @match *://*/*
|
||||
// @grant GM.xmlhttpRequest
|
||||
// @grant GM.openInTab
|
||||
// @noframes
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
GM.xmlhttpRequest({
|
||||
url: 'https://archive.is/' + document.location.href,
|
||||
method: 'GET',
|
||||
responseType: 'document',
|
||||
onloadend: processArchiveResponse
|
||||
});
|
||||
|
||||
function processArchiveResponse(response){
|
||||
const { response: archiveDocument } = response;
|
||||
const archiveURL = archiveDocument.querySelector('#CONTENT #row0 a')?.href;
|
||||
if (archiveURL) createArchiveButton(archiveURL);
|
||||
}
|
||||
|
||||
function createArchiveButton(archiveURL){
|
||||
let myDiv = document.createElement('div')
|
||||
myDiv.innerHTML = `<span href="#" style="all: initial; display: flex; justify-content: center; cursor: pointer; position: fixed; opacity: 0; transition: 1s; width: 60px; height: 60px; bottom: 40px; right: 40px; background-color: #0C9; color: #FFF; border-radius: 50px; text-align: center; box-shadow: 2px 2px 3px #999;">
|
||||
<h2 style="all: initial; align-self: center; font-size: 25px; color: white; cursor: pointer;">A</h2>
|
||||
</span>`;
|
||||
let spanEl = myDiv.children[0];
|
||||
document.body.append(spanEl)
|
||||
|
||||
spanEl.onclick = () => {
|
||||
document.location.href = archiveURL;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
spanEl.style.opacity = '1';
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
spanEl.style.opacity = '0';
|
||||
}, 5000);
|
||||
|
||||
setTimeout(() => {
|
||||
spanEl.remove();
|
||||
}, 6000);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// ==UserScript==
|
||||
// @name DarkReader
|
||||
// @version 0.0.7
|
||||
// @match *://*/*
|
||||
// @downloadURL https://gist.github.com/evanreichard/f983ca26836d4af6ca0f6fb602b9dcaf/raw/darkreader.user.js
|
||||
// @require https://unpkg.com/darkreader@4.9.46/darkreader.js
|
||||
// @grant GM.xmlhttpRequest
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM_getValue
|
||||
// @grant GM_setValue
|
||||
// @inject-into content
|
||||
// @noframes
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
/* globals DarkReader */
|
||||
|
||||
let options = {
|
||||
brightness: 70,
|
||||
contrast: 110,
|
||||
sepia: 50,
|
||||
};
|
||||
|
||||
let prefersDark =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
function toggleHost() {
|
||||
let disabledHosts = GM_getValue("disabledHosts", []);
|
||||
let isDisabled = disabledHosts.includes(window.location.hostname);
|
||||
|
||||
if (isDisabled) {
|
||||
disabledHosts = disabledHosts.filter(
|
||||
(item) => item !== window.location.hostname,
|
||||
);
|
||||
} else {
|
||||
disabledHosts.push(window.location.hostname);
|
||||
}
|
||||
|
||||
GM_setValue("disabledHosts", disabledHosts);
|
||||
applySettings();
|
||||
}
|
||||
|
||||
function toggleSystem() {
|
||||
let useSystem = GM_getValue("useSystem", true);
|
||||
GM_setValue("useSystem", !useSystem);
|
||||
applySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is necessary for CORS related requests. GM.xmlhttpRequest
|
||||
* bypasses that restriction.
|
||||
**/
|
||||
async function extensionFetch(url) {
|
||||
let responseData = await new Promise((resolve, reject) => {
|
||||
GM.xmlhttpRequest({
|
||||
url,
|
||||
responseType: "blob",
|
||||
onerror: reject,
|
||||
onload: resolve,
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(responseData.response);
|
||||
}
|
||||
|
||||
DarkReader.setFetchMethod(extensionFetch);
|
||||
|
||||
function applySettings() {
|
||||
GM.unregisterMenuCommand("System Mode - Enable");
|
||||
GM.unregisterMenuCommand("System Mode - Disable");
|
||||
GM.unregisterMenuCommand("DarkReader - Disable");
|
||||
GM.unregisterMenuCommand("DarkReader - Enable");
|
||||
|
||||
let disabledHosts = GM_getValue("disabledHosts", []);
|
||||
let useSystem = GM_getValue("useSystem", true);
|
||||
|
||||
if (useSystem) {
|
||||
GM.registerMenuCommand("System Mode - Disable", toggleSystem);
|
||||
} else {
|
||||
GM.registerMenuCommand("System Mode - Enable", toggleSystem);
|
||||
}
|
||||
|
||||
let isDisabled = disabledHosts.includes(window.location.hostname);
|
||||
if (isDisabled) {
|
||||
GM.registerMenuCommand("DarkReader - Enable", toggleHost);
|
||||
} else {
|
||||
GM.registerMenuCommand("DarkReader - Disable", toggleHost);
|
||||
}
|
||||
|
||||
if (useSystem && prefersDark && !isDisabled)
|
||||
return DarkReader.enable(options);
|
||||
if (!useSystem && !isDisabled) return DarkReader.enable(options);
|
||||
|
||||
DarkReader.disable();
|
||||
}
|
||||
|
||||
// System Change
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (event) => {
|
||||
prefersDark = event.matches;
|
||||
applySettings();
|
||||
});
|
||||
|
||||
// Initial Load
|
||||
applySettings();
|
||||
@@ -0,0 +1,249 @@
|
||||
// ==UserScript==
|
||||
// @name Em Dash Highlighter
|
||||
// @namespace Reichard
|
||||
// @version 1.0.0
|
||||
// @description Detects em dashes on the page, highlights them, and shows a popup on hover/tap
|
||||
// @author Evan Reichard
|
||||
// @match *://*/*
|
||||
// @grant GM_addStyle
|
||||
// @run-at document-idle
|
||||
// ==/UserScript==
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────
|
||||
|
||||
const EM_DASH = '\u2014';
|
||||
const EMDASH_REGEX = /\u2014/g;
|
||||
const POPUP_ID = 'emdash-popup';
|
||||
|
||||
// Tags whose text content we should never touch
|
||||
const SKIP_TAGS = new Set([
|
||||
'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT',
|
||||
'SELECT', 'BUTTON', 'PRE', 'SVG', 'MATH',
|
||||
]);
|
||||
|
||||
// ─── Styles ───────────────────────────────────────────────────────────────
|
||||
|
||||
GM_addStyle(`
|
||||
.emdash-highlight {
|
||||
display: inline;
|
||||
padding: 0 1px;
|
||||
border-radius: 2px;
|
||||
cursor: help;
|
||||
animation: emdash-hue 4s linear infinite;
|
||||
background-color: hsl(0, 80%, 70%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes emdash-hue {
|
||||
0% { background-color: hsl(0, 80%, 70%); }
|
||||
25% { background-color: hsl(90, 80%, 70%); }
|
||||
50% { background-color: hsl(180, 80%, 70%); }
|
||||
75% { background-color: hsl(270, 80%, 70%); }
|
||||
100% { background-color: hsl(360, 80%, 70%); }
|
||||
}
|
||||
|
||||
#${POPUP_ID} {
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
border: 1px solid #89b4fa;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font: 13px/1.4 monospace;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
max-width: 280px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#${POPUP_ID}.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
`);
|
||||
|
||||
// ─── Popup singleton ──────────────────────────────────────────────────────
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.id = POPUP_ID;
|
||||
popup.innerHTML = `
|
||||
<strong style="color:#89b4fa">Em Dash</strong> — U+2014
|
||||
`;
|
||||
document.documentElement.appendChild(popup);
|
||||
|
||||
let hideTimer = null;
|
||||
|
||||
function showPopup(anchorEl) {
|
||||
clearTimeout(hideTimer);
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
const popupHeight = popup.offsetHeight || 80;
|
||||
const popupWidth = popup.offsetWidth || 220;
|
||||
|
||||
let top = rect.top - popupHeight - 8;
|
||||
let left = rect.left + rect.width / 2 - popupWidth / 2;
|
||||
|
||||
// Flip below if not enough room above
|
||||
if (top < 4) top = rect.bottom + 8;
|
||||
|
||||
// Clamp horizontally
|
||||
left = Math.max(6, Math.min(left, window.innerWidth - popupWidth - 6));
|
||||
|
||||
popup.style.top = `${top}px`;
|
||||
popup.style.left = `${left}px`;
|
||||
popup.classList.add('visible');
|
||||
}
|
||||
|
||||
function hidePopup(delay = 120) {
|
||||
hideTimer = setTimeout(() => popup.classList.remove('visible'), delay);
|
||||
}
|
||||
|
||||
// ─── Span factory ─────────────────────────────────────────────────────────
|
||||
|
||||
function createSpan() {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'emdash-highlight';
|
||||
span.setAttribute('aria-label', 'Em dash character');
|
||||
span.textContent = EM_DASH;
|
||||
|
||||
// Desktop hover
|
||||
span.addEventListener('mouseenter', () => showPopup(span));
|
||||
span.addEventListener('mouseleave', () => hidePopup());
|
||||
|
||||
// Mobile tap — show/hide without bubbling
|
||||
span.addEventListener('touchend', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (popup.classList.contains('visible')) {
|
||||
hidePopup(0);
|
||||
} else {
|
||||
showPopup(span);
|
||||
// Auto-hide after 3 s on mobile
|
||||
hideTimer = setTimeout(() => popup.classList.remove('visible'), 3000);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Prevent click bubbling (e.g. accordion toggles, links)
|
||||
span.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
// ─── Core processor ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk a DOM subtree and wrap every em dash in a text node with a span.
|
||||
* We operate on Text nodes directly to avoid serialising/deserialising HTML.
|
||||
*/
|
||||
function processNode(root) {
|
||||
// TreeWalker is faster than querySelectorAll for text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
root,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode(node) {
|
||||
const parent = node.parentElement;
|
||||
if (!parent) return NodeFilter.FILTER_REJECT;
|
||||
if (SKIP_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||
if (parent.isContentEditable) return NodeFilter.FILTER_REJECT;
|
||||
if (parent.classList.contains('emdash-highlight')) return NodeFilter.FILTER_REJECT;
|
||||
if (!node.nodeValue.includes(EM_DASH)) return NodeFilter.FILTER_SKIP;
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Collect first — mutating the DOM while walking breaks the walker
|
||||
const textNodes = [];
|
||||
let node;
|
||||
while ((node = walker.nextNode())) textNodes.push(node);
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
splitAndWrap(textNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a text node around em dashes and replace it with a fragment
|
||||
* containing plain text nodes and highlight spans.
|
||||
*/
|
||||
function splitAndWrap(textNode) {
|
||||
const text = textNode.nodeValue;
|
||||
const parts = text.split(EMDASH_REGEX);
|
||||
|
||||
if (parts.length <= 1) return; // no em dash found
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
parts.forEach((part, i) => {
|
||||
if (part) fragment.appendChild(document.createTextNode(part));
|
||||
if (i < parts.length - 1) fragment.appendChild(createSpan());
|
||||
});
|
||||
|
||||
textNode.parentNode.replaceChild(fragment, textNode);
|
||||
}
|
||||
|
||||
// ─── Mutation observer ────────────────────────────────────────────────────
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'characterData') {
|
||||
// Text node changed in place
|
||||
const node = mutation.target;
|
||||
if (
|
||||
node.nodeValue?.includes(EM_DASH) &&
|
||||
node.parentElement &&
|
||||
!SKIP_TAGS.has(node.parentElement.tagName) &&
|
||||
!node.parentElement.classList.contains('emdash-highlight')
|
||||
) {
|
||||
splitAndWrap(node);
|
||||
}
|
||||
} else if (mutation.type === 'childList') {
|
||||
for (const added of mutation.addedNodes) {
|
||||
if (added.nodeType === Node.TEXT_NODE) {
|
||||
if (
|
||||
added.nodeValue?.includes(EM_DASH) &&
|
||||
added.parentElement &&
|
||||
!SKIP_TAGS.has(added.parentElement.tagName) &&
|
||||
!added.parentElement.classList.contains('emdash-highlight')
|
||||
) {
|
||||
splitAndWrap(added);
|
||||
}
|
||||
} else if (added.nodeType === Node.ELEMENT_NODE) {
|
||||
processNode(added);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Global tap-outside to dismiss popup ──────────────────────────────────
|
||||
|
||||
document.addEventListener('touchstart', () => hidePopup(0), { passive: true });
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function init() {
|
||||
processNode(document.body);
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
|
||||
// document-idle fires after DOMContentLoaded, but guard anyway
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
// ==UserScript==
|
||||
// @name Paste Enabler
|
||||
// @version 0.0.1
|
||||
// @description Re-enables paste functionality on websites
|
||||
// @author Evan Reichard
|
||||
// @match *://*/*
|
||||
// @grant GM.registerMenuCommand
|
||||
// @noframes
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const enablePasteFunction = function() {
|
||||
const events = ['paste', 'copy', 'cut'];
|
||||
events.forEach(function(event) {
|
||||
document.addEventListener(event, function(e) {
|
||||
e.stopImmediatePropagation();
|
||||
return true;
|
||||
}, true);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input, textarea').forEach(function(el) {
|
||||
el.setAttribute('oncopy', 'return true');
|
||||
el.setAttribute('onpaste', 'return true');
|
||||
el.setAttribute('oncut', 'return true');
|
||||
el.removeAttribute('readonly');
|
||||
});
|
||||
};
|
||||
|
||||
// Register the menu command
|
||||
GM.registerMenuCommand("Enable Paste", enablePasteFunction, "p");
|
||||
})();
|
||||
@@ -0,0 +1,139 @@
|
||||
// ==UserScript==
|
||||
// @name Hacker News Tweaks
|
||||
// @version 0.0.4
|
||||
// @downloadURL https://gist.githubusercontent.com/evanreichard/96d89512cbfe23e5d7a81479c482e6da/raw/hn-tweaks.user.js
|
||||
// @match *://news.ycombinator.com/*
|
||||
// @grant GM.addStyle
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM_getValue
|
||||
// @grant GM.setValue
|
||||
// @noframes
|
||||
// @run-at document-idle
|
||||
// ==/UserScript==
|
||||
|
||||
/**
|
||||
* This is a hacked together UserScript to add comment collapsing like old reddit.
|
||||
* It also adds the following CSS tweak: https://news.ycombinator.com/item?id=29494475
|
||||
**/
|
||||
|
||||
let activeStyleElement;
|
||||
|
||||
async function toggleStyle() {
|
||||
let isEnabled = GM_getValue("enabled", true);
|
||||
if (isEnabled === true) {
|
||||
GM.unregisterMenuCommand("Disable Style");
|
||||
GM.registerMenuCommand("Enable Style", toggleStyle);
|
||||
activeStyleElement?.remove();
|
||||
} else {
|
||||
GM.unregisterMenuCommand("Enable Style");
|
||||
GM.registerMenuCommand("Disable Style", toggleStyle);
|
||||
applyStyle();
|
||||
}
|
||||
await GM.setValue("enabled", !isEnabled);
|
||||
}
|
||||
|
||||
function applyStyle() {
|
||||
activeStyleElement = GM.addStyle(`.comhead {
|
||||
font-size: 12pt;
|
||||
line-height: 20pt;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) { body { background-color: #1F2430; }
|
||||
#hnmain { background-color: #232834; }
|
||||
|
||||
a:link.storylink,
|
||||
#hnmain > tbody > tr:first-child > td a,
|
||||
.commtext, .commtext a:link,
|
||||
td {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
#hnmain > tbody > tr:first-child > td { background-color: #333a4a; }
|
||||
|
||||
a:link,
|
||||
.sitebit, .subtext, .sitestr, .subtext a:link,
|
||||
.rank, a.morelink,
|
||||
.pagetop, .yclinks a,
|
||||
.comhead a:link, .comhead,
|
||||
.hnmore a:link, .reply a:link {
|
||||
color: #8c96ac;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #979cf4;
|
||||
}
|
||||
|
||||
.votearrow {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.votearrow::before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: 1px;
|
||||
top: 3px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 7px solid #bebfd1;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
textarea {
|
||||
background-color: #333a4a;
|
||||
color: #fafafa;
|
||||
border: 1px solid #1F2430;
|
||||
}
|
||||
}`);
|
||||
}
|
||||
|
||||
function findParent(el, index) {
|
||||
let previousSibling = el.previousElementSibling;
|
||||
let foundElem = previousSibling.querySelector(`[indent="${index}"]`);
|
||||
return foundElem
|
||||
? foundElem.closest(".athing")
|
||||
: findParent(previousSibling, index);
|
||||
}
|
||||
|
||||
let isEnabled = GM_getValue("enabled", true);
|
||||
if (isEnabled === true) {
|
||||
GM.registerMenuCommand("Disable Style", toggleStyle);
|
||||
applyStyle();
|
||||
} else {
|
||||
GM.registerMenuCommand("Enable Style", toggleStyle);
|
||||
}
|
||||
|
||||
Array.from(document.querySelectorAll(".ind")).forEach((indent) => {
|
||||
let imgEl = indent.querySelector("img");
|
||||
if (!imgEl) return;
|
||||
let colWidth = imgEl.offsetWidth;
|
||||
|
||||
indent.style.position = "relative";
|
||||
let indCount = parseInt(indent.getAttribute("indent"));
|
||||
|
||||
let voteOffset =
|
||||
indent.nextElementSibling.querySelector("center").offsetHeight;
|
||||
|
||||
for (let i = 0; i <= indCount; i += 1) {
|
||||
let marginLeft = i === 0 ? "10px" : (colWidth / indCount) * i + 10 + "px";
|
||||
let topOffset = i === indCount ? voteOffset + 15 : -10;
|
||||
let newElem = document.createElement("div");
|
||||
newElem.setAttribute(
|
||||
"style",
|
||||
`cursor: pointer; height: calc(100% - ${topOffset + 2}px); width: 0px; position: absolute; margin-left: ${marginLeft}; top: ${topOffset}px; border-left: 2px solid green; padding: 0px min(${colWidth / 2}px, 5px);`,
|
||||
);
|
||||
newElem.onclick = () => {
|
||||
let commentBlock = newElem.closest(".athing");
|
||||
if (i === indCount) {
|
||||
commentBlock.querySelector(".togg.clicky").click();
|
||||
} else {
|
||||
let parentComment = findParent(commentBlock, i);
|
||||
parentComment.querySelector(".togg.clicky").click();
|
||||
}
|
||||
};
|
||||
indent.appendChild(newElem);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// ==UserScript==
|
||||
// @name Auto Kagi LLM
|
||||
// @match https://kagi.com/assistant?mode=4*
|
||||
// @grant none
|
||||
// @version 1.0
|
||||
// @author Evan Reichard
|
||||
// ==/UserScript==
|
||||
|
||||
function doAutoLLM() {
|
||||
let hashLocation = window.location.hash;
|
||||
if (!hashLocation.startsWith("#query=")) return;
|
||||
|
||||
let query = decodeURIComponent(hashLocation.replace(/^#query=/, ''));
|
||||
let inputEl = document.querySelector("textarea");
|
||||
|
||||
inputEl.value = query;
|
||||
const keyboardEvent = new KeyboardEvent('keydown', {
|
||||
code: 'Enter',
|
||||
key: 'Enter',
|
||||
charCode: 13,
|
||||
keyCode: 13,
|
||||
view: window,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
inputEl.dispatchEvent(keyboardEvent);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
doAutoLLM()
|
||||
|
||||
// Example Usage:
|
||||
// https://kagi.com/assistant?mode=4&sub_mode=1#query=%s
|
||||
|
||||
// LLMs:
|
||||
// GPT35Turbo - https://kagi.com/assistant?mode=4&sub_mode=1
|
||||
// GPT4 - https://kagi.com/assistant?mode=4&sub_mode=2
|
||||
// GPT4Turbo - https://kagi.com/assistant?mode=4&sub_mode=7
|
||||
// Claude3Haiku - https://kagi.com/assistant?mode=4&sub_mode=13
|
||||
// Claude3Sonnet - https://kagi.com/assistant?mode=4&sub_mode=14
|
||||
// Claude3Opus - https://kagi.com/assistant?mode=4&sub_mode=12
|
||||
// MistralSmall - https://kagi.com/assistant?mode=4&sub_mode=10
|
||||
// MistralLarge - https://kagi.com/assistant?mode=4&sub_mode=11
|
||||
// Gemini15Pro - https://kagi.com/assistant?mode=4&sub_mode=8
|
||||
@@ -0,0 +1,854 @@
|
||||
// ==UserScript==
|
||||
// @name Reddit Tweaks (Apollo)
|
||||
// @version 0.3.0
|
||||
// @match *://old.reddit.com/*
|
||||
// @match *://www.reddit.com/*
|
||||
// @downloadURL https://gist.githubusercontent.com/evanreichard/968eee1cf83da38c81c155fb775cf3c6/raw/old-reddit-mobile.user.js
|
||||
// @grant GM.addStyle
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.unregisterMenuCommand
|
||||
// @grant GM.getValue
|
||||
// @grant GM.setValue
|
||||
// @grant GM_getValue
|
||||
// @noframes
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
/* ============================================================================
|
||||
* REDIRECT: www.reddit.com -> old.reddit.com
|
||||
* ----------------------------------------------------------------------------
|
||||
* Run before anything else so we don't waste cycles styling the new design.
|
||||
* The /media path is a special case: Reddit's image viewer wraps the image
|
||||
* in heavy chrome, so we strip everything down to just the <img> tag.
|
||||
* ========================================================================== */
|
||||
if (document.location.host === "www.reddit.com") {
|
||||
if (document.location.pathname === "/media") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const img = document.querySelector("img");
|
||||
if (img) document.body.innerHTML = img.outerHTML;
|
||||
});
|
||||
} else {
|
||||
window.location.href =
|
||||
"https://old.reddit.com" +
|
||||
window.location.pathname +
|
||||
window.location.search +
|
||||
window.location.hash;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* STATE
|
||||
* ----------------------------------------------------------------------------
|
||||
* `isEnabled` is read synchronously at script start so we can decide whether
|
||||
* to inject CSS at document-start (before paint). Toggling the script ALWAYS
|
||||
* reloads the page — see toggleStyle() for the reasoning.
|
||||
* ========================================================================== */
|
||||
let isEnabled = GM_getValue("enabled", true);
|
||||
|
||||
/* ============================================================================
|
||||
* STYLES
|
||||
* ----------------------------------------------------------------------------
|
||||
* All visual styling lives here. Uses CSS custom properties for theming so
|
||||
* dark mode is just a media query override on the :root tokens.
|
||||
* ========================================================================== */
|
||||
function applyStyle() {
|
||||
GM.addStyle(`
|
||||
/* --------------------------------------------------------------
|
||||
DESIGN TOKENS
|
||||
-------------------------------------------------------------- */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-elev: #f7f8fa;
|
||||
--bg-hover: #eef0f3;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f4f5f7;
|
||||
--border: #e3e6ea;
|
||||
--border-soft: #ecedef;
|
||||
|
||||
--fg: #1c1c1e;
|
||||
--fg-2: #3c3c43;
|
||||
--fg-dim: #6e6e73;
|
||||
--fg-faint: #b0b3b8;
|
||||
|
||||
--accent: #ff4500;
|
||||
--accent-2: #ff6a33;
|
||||
--link: #2f6feb;
|
||||
--visited: #8a8d96;
|
||||
|
||||
--upvote: #ff4500;
|
||||
--downvote: #5b8def;
|
||||
|
||||
/* Apollo-style thread depth colors (cycle every 6) */
|
||||
--depth-1: #4f9cf9; /* blue */
|
||||
--depth-2: #34c759; /* green */
|
||||
--depth-3: #ffcc00; /* yellow */
|
||||
--depth-4: #ff9f0a; /* orange */
|
||||
--depth-5: #ff453a; /* red */
|
||||
--depth-6: #bf5af2; /* purple */
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius: 10px;
|
||||
--radius-lg: 14px;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
|
||||
"Segoe UI", "Inter", Roboto, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
|
||||
--tap: 44px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0e0f12;
|
||||
--bg-elev: #16181d;
|
||||
--bg-hover: #1e2128;
|
||||
--surface: #16181d;
|
||||
--surface-2: #1c1f25;
|
||||
--border: #26292f;
|
||||
--border-soft: #1e2128;
|
||||
|
||||
--fg: #f2f2f7;
|
||||
--fg-2: #d1d1d6;
|
||||
--fg-dim: #9b9ba1;
|
||||
--fg-faint: #5a5d63;
|
||||
|
||||
--link: #5b8def;
|
||||
--visited: #6e6f78;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
GLOBAL RESET / BASE
|
||||
-------------------------------------------------------------- */
|
||||
html, body {
|
||||
background: var(--bg) !important;
|
||||
color: var(--fg) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
font-size: 15px !important;
|
||||
line-height: 1.4 !important;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a { color: var(--link); text-decoration: none; }
|
||||
a:visited { color: var(--visited); }
|
||||
a:hover { color: var(--fg); }
|
||||
|
||||
/* Hide all the cruft old.reddit ships with by default */
|
||||
.side,
|
||||
.footer-parent,
|
||||
.rank,
|
||||
.rank-spacer,
|
||||
#sr-header-area,
|
||||
.mobile-web-redirect-bar,
|
||||
.link.promotedlink,
|
||||
.infobar.listingsignupbar,
|
||||
.infobar.welcome,
|
||||
.listingsignupbar,
|
||||
.promoted,
|
||||
[data-promoted="true"],
|
||||
.res-floatingSideBar,
|
||||
.bottommenu,
|
||||
.sr-list,
|
||||
.sr-bar,
|
||||
.sr-interest-bar,
|
||||
.leavemoderator,
|
||||
.titlebox,
|
||||
.login-form-side,
|
||||
.icon-menu .gold-buy,
|
||||
.reportform,
|
||||
.panestack-title .res-comments-sortMenu,
|
||||
.link .flat-list.buttons li.share,
|
||||
.link .flat-list.buttons .post-sharing-button,
|
||||
.link .flat-list.buttons .reportbtn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
HEADER
|
||||
-------------------------------------------------------------- */
|
||||
#header {
|
||||
background: var(--bg-elev) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 0 12px !important;
|
||||
min-height: 48px !important;
|
||||
height: auto !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
backdrop-filter: saturate(180%) blur(12px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(12px);
|
||||
}
|
||||
#header-bottom-left {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px 14px;
|
||||
padding: 8px 0 !important;
|
||||
}
|
||||
#header-img, .pagename a {
|
||||
color: var(--fg) !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 17px !important;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.pagename { padding: 0 !important; }
|
||||
|
||||
.tabmenu {
|
||||
width: 100%;
|
||||
display: flex !important;
|
||||
gap: 4px;
|
||||
margin: 0 !important;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tabmenu::-webkit-scrollbar { display: none; }
|
||||
.tabmenu li { margin: 0 !important; }
|
||||
.tabmenu li a {
|
||||
display: inline-block;
|
||||
padding: 6px 12px !important;
|
||||
border-radius: 999px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 500;
|
||||
color: var(--fg-dim) !important;
|
||||
background: transparent !important;
|
||||
border: 1px solid transparent !important;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.tabmenu li.selected a {
|
||||
background: var(--accent) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.tabmenu li:not(.selected) a:hover {
|
||||
background: var(--bg-hover) !important;
|
||||
color: var(--fg) !important;
|
||||
}
|
||||
|
||||
#header-bottom-right {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg-dim) !important;
|
||||
font-size: 12px !important;
|
||||
padding: 6px 10px !important;
|
||||
border-left: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
#header-bottom-right a { color: var(--fg-dim) !important; }
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
CONTENT WRAPPER
|
||||
-------------------------------------------------------------- */
|
||||
.content[role="main"] {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
background: var(--bg) !important;
|
||||
color: var(--fg) !important;
|
||||
}
|
||||
#siteTable { margin: 0 !important; }
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
POSTS
|
||||
----------------------------------------------------------------
|
||||
Flex layout that preserves Reddit's native expando behavior.
|
||||
Order: thumbnail | entry | (votes hidden) | (wrap) expando
|
||||
-------------------------------------------------------------- */
|
||||
.link {
|
||||
position: relative;
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
column-gap: 12px;
|
||||
row-gap: 4px;
|
||||
padding: 12px 14px !important;
|
||||
margin: 0 !important;
|
||||
background: var(--surface) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 0 !important;
|
||||
border-bottom: 1px solid var(--border-soft) !important;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
.link:active { background: var(--bg-hover) !important; }
|
||||
.link.last-clicked {
|
||||
background: var(--bg-elev) !important;
|
||||
border-bottom: 1px solid var(--border-soft) !important;
|
||||
}
|
||||
|
||||
.link > .thumbnail { order: 1; flex: 0 0 auto; }
|
||||
.link > .entry { order: 2; flex: 1 1 0; min-width: 0; }
|
||||
.link > .midcol { order: 3; flex: 0 0 auto; }
|
||||
.link > .child,
|
||||
.link > .clearleft { display: none !important; }
|
||||
|
||||
/* Hide post-level vote arrows and native expando button (we tap thumbnail instead) */
|
||||
.link > .midcol { display: none !important; }
|
||||
.link .expando-button { display: none !important; }
|
||||
|
||||
/* Thumbnail container — shared by real and synthetic thumbs */
|
||||
.link > .thumbnail {
|
||||
width: 64px !important;
|
||||
height: 64px !important;
|
||||
margin: 0 !important;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border-soft);
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.link > .thumbnail img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Placeholder labels when there's no real image */
|
||||
.link > .thumbnail.self::after,
|
||||
.link > .thumbnail.default::after,
|
||||
.link > .thumbnail.spoiler::after,
|
||||
.link > .thumbnail.nsfw::after {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.link > .thumbnail.self::after { content: "TEXT"; }
|
||||
.link > .thumbnail.default::after { content: "LINK"; }
|
||||
.link > .thumbnail.spoiler::after { content: "⚠"; font-size: 18px; }
|
||||
.link.over18 > .thumbnail::after { content: "NSFW"; color: #ff453a; }
|
||||
|
||||
/* Entry / title / meta */
|
||||
.link .entry { margin: 0 !important; color: var(--fg) !important; }
|
||||
.link .top-matter { padding: 0 !important; margin: 0 !important; }
|
||||
|
||||
.link .title {
|
||||
margin: 0 0 4px 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: 15px !important;
|
||||
line-height: 1.3 !important;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.link .title a.title { color: var(--fg) !important; }
|
||||
.link.visited .title a.title,
|
||||
.link .title a.title:visited { color: var(--fg-dim) !important; }
|
||||
.link .title .domain {
|
||||
display: inline-block;
|
||||
font-size: 11px !important;
|
||||
color: var(--fg-dim) !important;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.link .title .domain a { color: var(--fg-dim) !important; }
|
||||
|
||||
.link .linkflairlabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 10.5px !important;
|
||||
font-weight: 600;
|
||||
padding: 2px 7px !important;
|
||||
border-radius: 999px;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.4;
|
||||
border: 0 !important;
|
||||
}
|
||||
.link .flairemoji { width: 12px !important; height: 12px !important; }
|
||||
|
||||
.link .tagline {
|
||||
font-size: 12px !important;
|
||||
color: var(--fg-dim) !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
.link .tagline a,
|
||||
.link .tagline time { color: var(--fg-dim) !important; }
|
||||
.link .tagline .author { color: var(--fg-2) !important; font-weight: 500; }
|
||||
|
||||
.link .flat-list.buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 4px 0 0 0 !important;
|
||||
padding: 0 !important;
|
||||
font-size: 12px !important;
|
||||
list-style: none;
|
||||
}
|
||||
.link .flat-list.buttons li { display: inline-block; margin: 0; padding: 0; }
|
||||
.link .flat-list.buttons a { color: var(--fg-dim) !important; font-weight: 500; }
|
||||
.link .flat-list.buttons .comments {
|
||||
color: var(--accent) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
EXPANDO (full-width row beneath the post)
|
||||
-------------------------------------------------------------- */
|
||||
.link > .expando {
|
||||
order: 4;
|
||||
flex: 0 0 100%;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 8px 0 0 0 !important;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
color: var(--fg) !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.link > .expando .media-preview,
|
||||
.link > .expando .media-preview-content,
|
||||
.link > .expando .res-expando-box,
|
||||
.link > .expando .res-expando-box div {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.link .expando img,
|
||||
.link .expando video,
|
||||
.link .expando iframe,
|
||||
.link .expando .preview,
|
||||
.link .expando .media-preview-content > * {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: var(--radius);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Self-text body */
|
||||
.link .expando .usertext-body,
|
||||
.link .usertext-body,
|
||||
.commentarea .link .usertext-body {
|
||||
background: var(--surface-2) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border-soft) !important;
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 14px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.link .expando .md,
|
||||
.link .usertext-body .md {
|
||||
background: transparent !important;
|
||||
color: var(--fg) !important;
|
||||
max-width: unset !important;
|
||||
font-size: 14px !important;
|
||||
line-height: 1.55 !important;
|
||||
}
|
||||
.link .expando .md p,
|
||||
.link .expando .md li,
|
||||
.link .expando .md h1,
|
||||
.link .expando .md h2,
|
||||
.link .expando .md h3,
|
||||
.link .expando .md blockquote {
|
||||
color: var(--fg) !important;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
COMMENTS — bubble-in-bubble with depth-colored left border
|
||||
-------------------------------------------------------------- */
|
||||
.commentarea {
|
||||
background: var(--bg) !important;
|
||||
color: var(--fg) !important;
|
||||
padding: 8px 10px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.commentarea > .menuarea {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
padding: 8px 4px !important;
|
||||
margin: 0 0 4px 0 !important;
|
||||
font-size: 12px;
|
||||
color: var(--fg-dim) !important;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: var(--surface) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border-soft) !important;
|
||||
border-left: 3px solid var(--depth-1) !important;
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px !important;
|
||||
margin: 6px 0 !important;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.comment .child,
|
||||
.comment .showreplies {
|
||||
border-left: 0 !important;
|
||||
margin: 6px 0 0 6px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Cycle border colors by nesting depth */
|
||||
.comment .comment { border-left-color: var(--depth-2) !important; }
|
||||
.comment .comment .comment { border-left-color: var(--depth-3) !important; }
|
||||
.comment .comment .comment .comment { border-left-color: var(--depth-4) !important; }
|
||||
.comment .comment .comment .comment .comment { border-left-color: var(--depth-5) !important; }
|
||||
.comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-6) !important; }
|
||||
.comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-1) !important; }
|
||||
.comment .comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-2) !important; }
|
||||
.comment .comment .comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-3) !important; }
|
||||
|
||||
/* Alternate bg shades to emphasize nesting */
|
||||
.comment .comment { background: var(--surface-2) !important; }
|
||||
.comment .comment .comment { background: var(--surface) !important; }
|
||||
|
||||
.comment .entry { padding: 0 !important; }
|
||||
.comment .tagline {
|
||||
font-size: 12px !important;
|
||||
color: var(--fg-dim) !important;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.comment .tagline .author { color: var(--fg) !important; font-weight: 600; }
|
||||
.comment .tagline .submitter { color: var(--link) !important; font-weight: 600; }
|
||||
.comment .tagline .score { color: var(--fg-dim) !important; font-weight: 500; }
|
||||
|
||||
.comment .usertext-body,
|
||||
.comment .usertext-body .md,
|
||||
.comment .md {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
color: var(--fg) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.comment .md p { margin: 4px 0 !important; color: var(--fg) !important; }
|
||||
.comment .md a { color: var(--link) !important; }
|
||||
.comment .md blockquote {
|
||||
border-left: 3px solid var(--border);
|
||||
padding-left: 10px;
|
||||
color: var(--fg-2) !important;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.comment .md code {
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.comment .md pre {
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.comment .midcol {
|
||||
width: 16px !important;
|
||||
margin-right: 4px !important;
|
||||
}
|
||||
.comment .arrow {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
background-size: 14px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.comment .arrow:hover { background-color: var(--bg-hover); }
|
||||
|
||||
.comment .flat-list.buttons {
|
||||
font-size: 12px !important;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
.comment .flat-list.buttons a { color: var(--fg-dim) !important; font-weight: 500; }
|
||||
.comment .flat-list.buttons .reply-button a { color: var(--link) !important; }
|
||||
|
||||
.comment .expand {
|
||||
color: var(--fg-dim) !important;
|
||||
font-weight: 600;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.comment.collapsed {
|
||||
opacity: 0.7;
|
||||
padding: 6px 12px !important;
|
||||
}
|
||||
|
||||
/* "load more" buttons */
|
||||
.morechildren a, .morecomments a {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--link) !important;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
PAGINATION
|
||||
-------------------------------------------------------------- */
|
||||
.nextprev {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 16px 14px 28px;
|
||||
margin: 0;
|
||||
}
|
||||
.nextprev .separator { display: none; }
|
||||
.nextprev a {
|
||||
flex: 1 1 0;
|
||||
min-height: var(--tap);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.06s ease, background 0.15s ease;
|
||||
}
|
||||
.nextprev a:hover { background: var(--bg-hover) !important; }
|
||||
.nextprev a:active { transform: scale(0.98); }
|
||||
.nextprev a[rel*="next"] {
|
||||
background: var(--accent) !important;
|
||||
color: #fff !important;
|
||||
border-color: transparent;
|
||||
}
|
||||
.nextprev a[rel*="next"]:hover { background: var(--accent-2) !important; }
|
||||
|
||||
/* --------------------------------------------------------------
|
||||
FORMS / MENUS / MISC
|
||||
-------------------------------------------------------------- */
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=email],
|
||||
textarea {
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
padding: 8px 10px !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.btn, button, .morelink a {
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
padding: 6px 12px !important;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
.menuarea, .panestack-title {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
color: var(--fg-dim) !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
.dropdown, .drop-choices {
|
||||
background: var(--surface) !important;
|
||||
color: var(--fg) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
background: var(--surface) !important;
|
||||
color: var(--fg) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
pre, code {
|
||||
background: var(--bg-elev) !important;
|
||||
color: var(--fg) !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
|
||||
/* Dim Reddit's default vote arrows in dark mode (they're black PNGs) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.arrow:not(.upmod):not(.downmod) { filter: brightness(0.85) invert(0.9); }
|
||||
}
|
||||
|
||||
a, button { touch-action: manipulation; }
|
||||
body > .content .link .midcol { width: auto !important; }
|
||||
body > .content .link .rank, .rank-spacer { width: 0 !important; }
|
||||
`);
|
||||
|
||||
// Mobile viewport — Reddit's default doesn't include viewport-fit=cover,
|
||||
// which we want for safe-area handling on notched devices.
|
||||
if (document.head) {
|
||||
const meta = document.createElement("meta");
|
||||
meta.setAttribute("name", "viewport");
|
||||
meta.setAttribute(
|
||||
"content",
|
||||
"width=device-width, initial-scale=1, viewport-fit=cover",
|
||||
);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
|
||||
// Nuke subreddit-specific stylesheet ASAP so it can't fight ours
|
||||
document.head?.querySelector("[ref=applied_subreddit_stylesheet]")?.remove();
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* DOM TWEAKS
|
||||
* ----------------------------------------------------------------------------
|
||||
* Things CSS alone can't do: rewriting nodes, injecting placeholders,
|
||||
* relocating elements within the post layout.
|
||||
* ========================================================================== */
|
||||
function applyDOMTweaks() {
|
||||
// Belt-and-suspenders subreddit stylesheet removal (in case it loaded after us)
|
||||
document.querySelector('link[title="applied_subreddit_stylesheet"]')?.remove();
|
||||
|
||||
// Replace the subreddit logo/banner anchor with a plain "reddit" home link
|
||||
const headerAnchor = document.querySelector("#header-bottom-left a");
|
||||
if (headerAnchor) {
|
||||
headerAnchor.outerHTML =
|
||||
`<a href="/" id="header-img" class="default-header" title="">reddit</a>`;
|
||||
}
|
||||
|
||||
// Process every post on the page
|
||||
document.querySelectorAll(".link").forEach(processLink);
|
||||
|
||||
// Pagination polish — shorten "view more" wording and add arrows
|
||||
const nextprev = document.querySelector(".nextprev");
|
||||
if (nextprev) {
|
||||
// The first text node is often "view more:" — drop it
|
||||
const first = nextprev.childNodes[0];
|
||||
if (first && first.textContent.trim().toLowerCase().startsWith("view more")) {
|
||||
first.remove();
|
||||
}
|
||||
nextprev.querySelectorAll("a").forEach((a) => {
|
||||
const rel = (a.getAttribute("rel") || "").toLowerCase();
|
||||
if (rel.includes("next")) a.textContent = "Next ›";
|
||||
else if (rel.includes("prev")) a.textContent = "‹ Prev";
|
||||
});
|
||||
// Center the lone button if there's only one direction available
|
||||
if (nextprev.querySelectorAll("a").length === 1) {
|
||||
nextprev.style.justifyContent = "center";
|
||||
nextprev.querySelector("a").style.flex = "0 1 280px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-post tweaks:
|
||||
* 1. Move .expando out of .entry so it can become a full-width flex row
|
||||
* (flex order: thumbnail | entry | expando-on-new-line).
|
||||
* 2. Inject a synthetic .thumbnail <div> when the subreddit suppresses
|
||||
* thumbnails server-side (e.g. r/cars). This guarantees every post
|
||||
* has a tappable thumb area showing "TEXT", "LINK", "NSFW", etc.
|
||||
*/
|
||||
function processLink(link) {
|
||||
// --- 1. Promote .expando to be a direct child of .link ---
|
||||
const expando = link.querySelector(":scope > .entry > .expando");
|
||||
if (expando) link.appendChild(expando);
|
||||
|
||||
// --- 2. Inject synthetic thumbnail if missing ---
|
||||
if (!link.querySelector(":scope > .thumbnail")) {
|
||||
const thumb = document.createElement("div");
|
||||
|
||||
// Pick the right placeholder class so CSS ::after shows the right label
|
||||
if (link.classList.contains("over18")) {
|
||||
thumb.className = "thumbnail nsfw";
|
||||
} else if (link.classList.contains("spoiler")) {
|
||||
thumb.className = "thumbnail spoiler";
|
||||
} else if (link.classList.contains("self")) {
|
||||
thumb.className = "thumbnail self";
|
||||
} else {
|
||||
thumb.className = "thumbnail default";
|
||||
}
|
||||
|
||||
link.prepend(thumb);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* INTERACTION HANDLERS
|
||||
* ----------------------------------------------------------------------------
|
||||
* Delegated click handlers — attached once at script start, gated on
|
||||
* `isEnabled` so they no-op when the user disables the script live.
|
||||
* ========================================================================== */
|
||||
|
||||
// Tap a comment body to collapse/expand it (Apollo-style)
|
||||
document.addEventListener("click", (evt) => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
// Ignore clicks on interactive descendants
|
||||
if (evt.target.closest("a")) return;
|
||||
if (evt.target.classList.contains("expand")) return;
|
||||
if (evt.target.closest(".flat-list.buttons")) return;
|
||||
if (evt.target.closest(".usertext-edit")) return;
|
||||
if (evt.target.closest(".midcol")) return;
|
||||
|
||||
const comment = evt.target.closest(".comment");
|
||||
if (!comment) return;
|
||||
|
||||
const expander = comment.querySelector(":scope > .entry .expand");
|
||||
if (!expander) return;
|
||||
expander.click();
|
||||
});
|
||||
|
||||
// Tap a thumbnail to toggle the post's expando.
|
||||
// On synthetic thumbnails (link posts with no expando) this is a no-op.
|
||||
document.addEventListener("click", (evt) => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
let thumb;
|
||||
if (evt.target.classList?.contains("thumbnail")) {
|
||||
thumb = evt.target;
|
||||
} else if (evt.target.parentElement?.classList?.contains("thumbnail")) {
|
||||
thumb = evt.target.parentElement;
|
||||
}
|
||||
if (!thumb) return;
|
||||
|
||||
const link = thumb.closest(".link");
|
||||
if (!link) return;
|
||||
|
||||
const btn = link.querySelector(".expando-button");
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (!btn) return; // synthetic thumb, nothing to expand
|
||||
|
||||
btn.click();
|
||||
});
|
||||
|
||||
/* ============================================================================
|
||||
* MENU / TOGGLE
|
||||
* ----------------------------------------------------------------------------
|
||||
* Toggling always reloads the page. Why:
|
||||
* - GM.addStyle injects a <style> tag that can't be cleanly removed without
|
||||
* tracking references and tearing them down.
|
||||
* - DOM tweaks (moved expandos, injected thumbnails, replaced header anchor)
|
||||
* can't be reversed without remembering the original state.
|
||||
* - A reload gives a clean, predictable state for both directions.
|
||||
* ========================================================================== */
|
||||
async function toggleStyle() {
|
||||
const currentlyEnabled = await GM.getValue("enabled", true);
|
||||
await GM.setValue("enabled", !currentlyEnabled);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function registerMenu() {
|
||||
const label = isEnabled ? "Style - Disable" : "Style - Enable";
|
||||
GM.registerMenuCommand(label, toggleStyle);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* BOOT
|
||||
* ========================================================================== */
|
||||
if (isEnabled) {
|
||||
applyStyle(); // inject CSS at document-start
|
||||
window.addEventListener("load", applyDOMTweaks); // DOM tweaks once page is ready
|
||||
}
|
||||
registerMenu();
|
||||
@@ -0,0 +1,516 @@
|
||||
// ==UserScript==
|
||||
// @name Page Agent - LLM in the page
|
||||
// @namespace https://github.com/example/page-agent
|
||||
// @version 0.1.0
|
||||
// @description Turn any web page into an LLM agent that can run JS in the page to inspect, extract, fetch and act - then bake what it learned into a standalone, LLM-free userscript.
|
||||
// @match *://*/*
|
||||
// @run-at document-idle
|
||||
// @grant GM.xmlHttpRequest
|
||||
// @grant GM.setValue
|
||||
// @grant GM.getValue
|
||||
// @connect api.openai.com
|
||||
// @connect *
|
||||
// ==/UserScript==
|
||||
|
||||
// SECURITY NOTE: `@connect *` allows GM.xmlHttpRequest to POST to ANY host. That is
|
||||
// handy while you experiment with self-hosted / proxied endpoints, but it is broad.
|
||||
// Once you know your endpoint's host, DELETE the `@connect *` line above and keep
|
||||
// only the specific host(s) you use (e.g. just `@connect api.openai.com`).
|
||||
|
||||
(async () => {
|
||||
'use strict';
|
||||
|
||||
// Built at runtime so this file contains no literal triple-backticks and stays paste-safe.
|
||||
const BT3 = '`'.repeat(3);
|
||||
|
||||
// ============================================================
|
||||
// Configuration
|
||||
// ============================================================
|
||||
const DEFAULTS = {
|
||||
endpoint: 'https://api.openai.com/v1/chat/completions',
|
||||
apiKey: '',
|
||||
model: 'gpt-4o-mini',
|
||||
};
|
||||
|
||||
const CONFIG = {
|
||||
endpoint: await GM.getValue('endpoint', DEFAULTS.endpoint),
|
||||
apiKey: await GM.getValue('apiKey', DEFAULTS.apiKey),
|
||||
model: await GM.getValue('model', DEFAULTS.model),
|
||||
};
|
||||
|
||||
const maxSteps = 8; // upper bound on the agent's tool-use loop
|
||||
|
||||
// The panel starts fully hidden. Reveal it with:
|
||||
// Desktop - Ctrl+Shift+Space (see HOTKEY)
|
||||
// Mobile - three-finger tap anywhere on the page
|
||||
const HOTKEY = { ctrl: true, shift: true, alt: false, code: 'Space', label: 'Ctrl+Shift+Space' };
|
||||
|
||||
// ============================================================
|
||||
// safeStringify - serialize arbitrary values without blowing up
|
||||
// ============================================================
|
||||
function safeStringify(value, maxLen = 20000) {
|
||||
const seen = new WeakSet();
|
||||
let out;
|
||||
try {
|
||||
out = JSON.stringify(value, function (key, val) {
|
||||
// DOM nodes -> "[NODENAME] <first 200 chars of textContent>"
|
||||
if (typeof Node !== 'undefined' && val instanceof Node) {
|
||||
return '[' + val.nodeName + '] ' + String(val.textContent || '').slice(0, 200);
|
||||
}
|
||||
if (typeof val === 'function') return '[Function]';
|
||||
if (val !== null && typeof val === 'object') {
|
||||
if (seen.has(val)) return String(val); // circular / repeated ref
|
||||
seen.add(val);
|
||||
}
|
||||
return val;
|
||||
});
|
||||
} catch (e) {
|
||||
out = String(value);
|
||||
}
|
||||
if (out === undefined) out = String(value); // undefined coercion
|
||||
if (out.length > maxLen) {
|
||||
const total = out.length;
|
||||
out = out.slice(0, maxLen) + '...[truncated, ' + total + ' chars]';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// The single tool: run_js
|
||||
// ============================================================
|
||||
const TOOLS = [{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'run_js',
|
||||
description:
|
||||
'Execute JavaScript in the context of the current web page. Your code runs ' +
|
||||
'as the BODY of an async function, so you can use `await` and you MUST `return` ' +
|
||||
'a value to see any result. The returned value is JSON-stringified and sent back ' +
|
||||
'to you, so keep it SMALL and RELEVANT - return only the specific fields/text you ' +
|
||||
'need, never whole DOM trees or huge arrays. Use it to query selectors, extract ' +
|
||||
'content, call fetch(), read/modify the page, click, fill and navigate. Take ' +
|
||||
'several small targeted steps rather than one giant snippet.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'JavaScript run as the body of an async function. `return` the result you want back.',
|
||||
},
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
async function runJs(code) {
|
||||
try {
|
||||
// Wrapped so the model can `await` and `return` naturally.
|
||||
const fn = new Function('return (async () => { ' + code + ' })()');
|
||||
const result = await fn();
|
||||
return safeStringify(result);
|
||||
} catch (e) {
|
||||
return 'ERROR: ' + (e && e.stack ? e.stack : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OpenAI-compatible request via GM.xmlHttpRequest
|
||||
// ============================================================
|
||||
function chat(messages) {
|
||||
return new Promise((resolve, reject) => {
|
||||
GM.xmlHttpRequest({
|
||||
method: 'POST',
|
||||
url: CONFIG.endpoint,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + CONFIG.apiKey,
|
||||
},
|
||||
data: JSON.stringify({
|
||||
model: CONFIG.model,
|
||||
messages: messages,
|
||||
tools: TOOLS,
|
||||
tool_choice: 'auto',
|
||||
temperature: 0.2,
|
||||
}),
|
||||
timeout: 60000,
|
||||
onload: (res) => {
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
reject(new Error('HTTP ' + res.status + ': ' + res.responseText));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(res.responseText));
|
||||
} catch (e) {
|
||||
reject(new Error('Bad JSON from endpoint: ' + res.responseText));
|
||||
}
|
||||
},
|
||||
onerror: () => reject(new Error('Network error contacting ' + CONFIG.endpoint)),
|
||||
ontimeout: () => reject(new Error('Request timed out after 60s')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Agentic loop
|
||||
// ============================================================
|
||||
const SYSTEM_PROMPT =
|
||||
'You are an agent embedded directly in a live web page in the user\'s browser. ' +
|
||||
'You have exactly one tool, run_js, which executes JavaScript in this page and ' +
|
||||
'returns its JSON-stringified return value. Use it to inspect the DOM, query ' +
|
||||
'selectors, extract content, call fetch(), and act on the page (click, fill, ' +
|
||||
'navigate) to satisfy the request. Prefer small, targeted snippets that return ' +
|
||||
'only the specific data you need; do not dump whole DOM trees or large arrays. ' +
|
||||
'Take several small steps rather than one giant one. When the request is complete, ' +
|
||||
'stop calling tools and reply to the user in plain text.';
|
||||
|
||||
// Returns { answer, messages } so the conversation can be reused for later turns.
|
||||
async function agent(userRequest, log = console.log, history = null) {
|
||||
const messages = history ? history : [{ role: 'system', content: SYSTEM_PROMPT }];
|
||||
if (userRequest) messages.push({ role: 'user', content: userRequest });
|
||||
|
||||
for (let step = 0; step < maxSteps; step++) {
|
||||
const data = await chat(messages);
|
||||
const msg = data && data.choices && data.choices[0] && data.choices[0].message;
|
||||
if (!msg) {
|
||||
return { answer: 'ERROR: no message in response: ' + safeStringify(data), messages };
|
||||
}
|
||||
messages.push(msg);
|
||||
|
||||
const toolCalls = msg.tool_calls;
|
||||
if (!toolCalls || toolCalls.length === 0) {
|
||||
return { answer: msg.content || '', messages }; // final plain-text answer
|
||||
}
|
||||
|
||||
for (const tc of toolCalls) {
|
||||
let args = {};
|
||||
try {
|
||||
args = JSON.parse(tc.function.arguments || '{}');
|
||||
} catch (e) {
|
||||
args = { code: '' };
|
||||
}
|
||||
const code = args.code || '';
|
||||
log('>> run_js:\n' + code);
|
||||
const result = await runJs(code);
|
||||
log('<< result: ' + result.slice(0, 500));
|
||||
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
|
||||
}
|
||||
}
|
||||
|
||||
return { answer: '(Stopped after ' + maxSteps + ' steps without a final answer.)', messages };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UI - floating panel (bottom-right, dark), hidden by default
|
||||
// ============================================================
|
||||
const PANEL_ID = 'llm-page-agent-panel';
|
||||
if (document.getElementById(PANEL_ID)) return; // avoid double-inject
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#${PANEL_ID}{position:fixed;bottom:16px;right:16px;z-index:2147483647;width:340px;
|
||||
font-family:-apple-system,system-ui,Segoe UI,Roboto,sans-serif;font-size:13px;
|
||||
background:#1e1e22;color:#e6e6e6;border:1px solid #3a3a40;border-radius:10px;
|
||||
box-shadow:0 8px 30px rgba(0,0,0,.5);overflow:hidden;
|
||||
transition:opacity .12s ease, transform .12s ease;}
|
||||
#${PANEL_ID}.pa-hidden{opacity:0;transform:translateY(8px) scale(.98);pointer-events:none;
|
||||
visibility:hidden;}
|
||||
#${PANEL_ID} .pa-header{display:flex;align-items:center;gap:8px;padding:8px 10px;
|
||||
background:#27272c;user-select:none;}
|
||||
#${PANEL_ID} .pa-title{font-weight:600;flex:1;}
|
||||
#${PANEL_ID} .pa-iconbtn{background:transparent;border:none;color:#bdbdbd;cursor:pointer;
|
||||
font-size:14px;line-height:1;padding:3px 7px;border-radius:6px;}
|
||||
#${PANEL_ID} .pa-iconbtn:hover{background:#3a3a40;color:#fff;}
|
||||
#${PANEL_ID} .pa-body{padding:8px 10px;}
|
||||
#${PANEL_ID} .pa-log{max-height:240px;overflow-y:auto;white-space:pre-wrap;
|
||||
word-break:break-word;background:#131316;border:1px solid #2c2c31;border-radius:6px;
|
||||
padding:8px;line-height:1.35;margin-bottom:8px;font-family:ui-monospace,Menlo,Consolas,monospace;
|
||||
font-size:12px;}
|
||||
#${PANEL_ID} .pa-row{display:flex;gap:6px;margin-bottom:6px;}
|
||||
#${PANEL_ID} input.pa-input{flex:1;min-width:0;background:#131316;color:#e6e6e6;
|
||||
border:1px solid #3a3a40;border-radius:6px;padding:6px 8px;outline:none;}
|
||||
#${PANEL_ID} input.pa-input:disabled{opacity:.5;}
|
||||
#${PANEL_ID} button.pa-btn{background:#3b82f6;color:#fff;border:none;border-radius:6px;
|
||||
padding:6px 10px;cursor:pointer;font-size:12px;white-space:nowrap;}
|
||||
#${PANEL_ID} button.pa-btn.pa-secondary{background:#3a3a40;}
|
||||
#${PANEL_ID} button.pa-btn:disabled{opacity:.5;cursor:not-allowed;}
|
||||
#${PANEL_ID} .pa-spinner{display:inline-block;width:12px;height:12px;
|
||||
border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;
|
||||
animation:pa-rotation .7s linear infinite;vertical-align:middle;}
|
||||
#${PANEL_ID}.pa-collapsed .pa-body{display:none;}
|
||||
@keyframes pa-rotation{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
|
||||
|
||||
#pa-agent-toast{position:fixed;bottom:16px;right:16px;z-index:2147483647;
|
||||
font-family:-apple-system,system-ui,Segoe UI,Roboto,sans-serif;font-size:12px;
|
||||
background:#1e1e22;color:#e6e6e6;border:1px solid #3a3a40;border-radius:8px;
|
||||
padding:8px 12px;box-shadow:0 6px 20px rgba(0,0,0,.45);max-width:280px;
|
||||
transition:opacity .3s ease;pointer-events:none;}
|
||||
#pa-agent-toast.pa-fade{opacity:0;}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = PANEL_ID;
|
||||
panel.className = 'pa-hidden'; // hidden until revealed
|
||||
panel.innerHTML =
|
||||
'<div class="pa-header">' +
|
||||
'<span class="pa-title">[AI] Page Agent</span>' +
|
||||
'<button class="pa-iconbtn" data-act="settings" title="Settings">[cfg]</button>' +
|
||||
'<button class="pa-iconbtn" data-act="collapse" title="Collapse">-</button>' +
|
||||
'<button class="pa-iconbtn" data-act="hide" title="Hide (reopen with ' + HOTKEY.label + ' or a three-finger tap)">x</button>' +
|
||||
'</div>' +
|
||||
'<div class="pa-body">' +
|
||||
'<div class="pa-log"></div>' +
|
||||
'<div class="pa-row">' +
|
||||
'<input class="pa-input" type="text" placeholder="Ask the agent to do something..." />' +
|
||||
'<button class="pa-btn" data-act="send">Send</button>' +
|
||||
'</div>' +
|
||||
'<div class="pa-row">' +
|
||||
'<button class="pa-btn pa-secondary" data-act="convert">Convert to userscript</button>' +
|
||||
'<button class="pa-btn pa-secondary" data-act="download">Download .user.js</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(panel);
|
||||
|
||||
const logEl = panel.querySelector('.pa-log');
|
||||
const inputEl = panel.querySelector('.pa-input');
|
||||
const sendBtn = panel.querySelector('[data-act="send"]');
|
||||
const convertBtn = panel.querySelector('[data-act="convert"]');
|
||||
const downloadBtn = panel.querySelector('[data-act="download"]');
|
||||
const settingsBtn = panel.querySelector('[data-act="settings"]');
|
||||
const collapseBtn = panel.querySelector('[data-act="collapse"]');
|
||||
const hideBtn = panel.querySelector('[data-act="hide"]');
|
||||
const allButtons = [sendBtn, convertBtn, downloadBtn];
|
||||
|
||||
function append(text) {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = text;
|
||||
logEl.appendChild(line);
|
||||
logEl.scrollTop = logEl.scrollHeight; // autoscroll
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Visibility - hidden by default; toggle via hotkey / gesture
|
||||
// ============================================================
|
||||
let hintShown = false;
|
||||
function isHidden() { return panel.classList.contains('pa-hidden'); }
|
||||
|
||||
function showPanel() {
|
||||
panel.classList.remove('pa-hidden');
|
||||
if (!hintShown) {
|
||||
append('Reveal/hide me anytime: ' + HOTKEY.label + ' (desktop) or a three-finger tap (mobile).');
|
||||
hintShown = true;
|
||||
}
|
||||
setTimeout(() => inputEl.focus(), 0);
|
||||
}
|
||||
|
||||
function hidePanel() { panel.classList.add('pa-hidden'); }
|
||||
|
||||
function togglePanel() { if (isHidden()) showPanel(); else hidePanel(); }
|
||||
|
||||
// Transient toast so the reveal shortcut is discoverable on page load.
|
||||
function showToast(text, ms = 4000) {
|
||||
const existing = document.getElementById('pa-agent-toast');
|
||||
if (existing) existing.remove();
|
||||
const toast = document.createElement('div');
|
||||
toast.id = 'pa-agent-toast';
|
||||
toast.textContent = text;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('pa-fade');
|
||||
setTimeout(() => toast.remove(), 320);
|
||||
}, ms);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Busy / loading state
|
||||
// ============================================================
|
||||
let busy = false;
|
||||
let statusEl = null;
|
||||
function setBusy(on, label) {
|
||||
busy = on;
|
||||
inputEl.disabled = on;
|
||||
allButtons.forEach((b) => { b.disabled = on; });
|
||||
if (on) {
|
||||
sendBtn.dataset.label = sendBtn.textContent;
|
||||
sendBtn.innerHTML = '<span class="pa-spinner"></span>';
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.innerHTML = '<span class="pa-spinner"></span> ' + (label || 'thinking...');
|
||||
logEl.appendChild(statusEl);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
} else {
|
||||
if (sendBtn.dataset.label) sendBtn.textContent = sendBtn.dataset.label;
|
||||
if (statusEl && statusEl.parentNode) statusEl.parentNode.removeChild(statusEl);
|
||||
statusEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Conversation state + runners
|
||||
// ============================================================
|
||||
let conversation = null; // running messages array, reused across turns
|
||||
let lastCode = ''; // last generated userscript
|
||||
|
||||
async function run(request) {
|
||||
if (busy) return;
|
||||
if (!CONFIG.apiKey) { append('! No API key set. Click [cfg] to configure.'); return; }
|
||||
append('> ' + request);
|
||||
setBusy(true, 'thinking...');
|
||||
try {
|
||||
const { answer, messages } = await agent(request, append, conversation);
|
||||
conversation = messages; // persist for later turns
|
||||
append('[AI] ' + answer);
|
||||
} catch (e) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
setBusy(false); // always reset
|
||||
}
|
||||
}
|
||||
|
||||
function send() {
|
||||
const v = inputEl.value.trim();
|
||||
if (!v) return;
|
||||
inputEl.value = '';
|
||||
run(v);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Convert / Download - bake the session into a standalone userscript
|
||||
// ============================================================
|
||||
const CONVERT_PROMPT =
|
||||
'Now convert EVERYTHING you learned and did in this session into a single, ' +
|
||||
'standalone, repeatable Tampermonkey/Violentmonkey userscript that performs the ' +
|
||||
'same task automatically with NO LLM calls and no dependence on this agent. Bake ' +
|
||||
'in the concrete selectors, fetch() URLs, and data transforms you discovered. ' +
|
||||
'Include a complete ==UserScript== metadata header (@name, @namespace, @version, ' +
|
||||
'@description, an appropriate @match, @run-at and any @grant/@connect you need). ' +
|
||||
'The script must be self-contained and idempotent. Output ONLY the finished ' +
|
||||
'userscript inside a single ' + BT3 + 'javascript code block - no explanation ' +
|
||||
'before or after.';
|
||||
|
||||
function extractCode(text) {
|
||||
if (!text) return '';
|
||||
const re = new RegExp(BT3 + 'javascript\\s*([\\s\\S]*?)' + BT3, 'i');
|
||||
const m = text.match(re);
|
||||
if (m) return m[1].trim();
|
||||
return text.trim(); // fallback: raw text
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fall through to fallback */ }
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (e) { /* ignore */ }
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
|
||||
function saveFile(text, filename) {
|
||||
const blob = new Blob([text], { type: 'text/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Runs the convert prompt against a COPY of the conversation so the live one stays clean.
|
||||
async function generateUserscript() {
|
||||
const copy = JSON.parse(JSON.stringify(conversation));
|
||||
const { answer } = await agent(CONVERT_PROMPT, append, copy);
|
||||
lastCode = extractCode(answer);
|
||||
return lastCode;
|
||||
}
|
||||
|
||||
async function convert() {
|
||||
if (busy) return;
|
||||
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||
setBusy(true, 'converting...');
|
||||
try {
|
||||
const code = await generateUserscript();
|
||||
await copyToClipboard(code);
|
||||
append('OK Userscript generated & copied to clipboard (' + code.length + ' chars). Use "Download .user.js" to save it.');
|
||||
} catch (e) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (busy) return;
|
||||
if (!conversation) { append('! Nothing to convert yet - run the agent first.'); return; }
|
||||
setBusy(true, 'preparing download...');
|
||||
try {
|
||||
const code = lastCode || await generateUserscript();
|
||||
saveFile(code, 'page-agent-generated.user.js');
|
||||
append('v Saved page-agent-generated.user.js (' + code.length + ' chars).');
|
||||
} catch (e) {
|
||||
append('ERROR: ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Settings
|
||||
// ============================================================
|
||||
async function openSettings() {
|
||||
const ep = prompt('Endpoint (chat-completions URL):', CONFIG.endpoint);
|
||||
if (ep !== null) { CONFIG.endpoint = ep.trim() || DEFAULTS.endpoint; await GM.setValue('endpoint', CONFIG.endpoint); }
|
||||
const key = prompt('API key (Bearer token):', CONFIG.apiKey);
|
||||
if (key !== null) { CONFIG.apiKey = key.trim(); await GM.setValue('apiKey', CONFIG.apiKey); }
|
||||
const mdl = prompt('Model:', CONFIG.model);
|
||||
if (mdl !== null) { CONFIG.model = mdl.trim() || DEFAULTS.model; await GM.setValue('model', CONFIG.model); }
|
||||
append('[cfg] Settings saved - endpoint=' + CONFIG.endpoint + ' model=' + CONFIG.model + ' key=' + (CONFIG.apiKey ? '(set)' : '(empty)'));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Wiring
|
||||
// ============================================================
|
||||
sendBtn.addEventListener('click', send);
|
||||
inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });
|
||||
convertBtn.addEventListener('click', convert);
|
||||
downloadBtn.addEventListener('click', download);
|
||||
settingsBtn.addEventListener('click', openSettings);
|
||||
hideBtn.addEventListener('click', hidePanel);
|
||||
collapseBtn.addEventListener('click', () => {
|
||||
const collapsed = panel.classList.toggle('pa-collapsed');
|
||||
collapseBtn.textContent = collapsed ? '[ ]' : '-';
|
||||
collapseBtn.title = collapsed ? 'Expand' : 'Collapse';
|
||||
});
|
||||
|
||||
// Desktop reveal/hide - global hotkey (Ctrl+Shift+Space by default).
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (!!e.ctrlKey === HOTKEY.ctrl && !!e.shiftKey === HOTKEY.shift &&
|
||||
!!e.altKey === HOTKEY.alt && e.code === HOTKEY.code) {
|
||||
e.preventDefault();
|
||||
togglePanel();
|
||||
} else if (e.key === 'Escape' && !isHidden() && document.activeElement !== inputEl) {
|
||||
hidePanel();
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Mobile reveal/hide - three-finger tap anywhere on the page.
|
||||
window.addEventListener('touchstart', (e) => {
|
||||
if (e.touches && e.touches.length === 3) {
|
||||
e.preventDefault();
|
||||
togglePanel();
|
||||
}
|
||||
}, { capture: true, passive: false });
|
||||
|
||||
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via [cfg] first. ') + 'Ask me to inspect or act on this page.');
|
||||
showToast('[AI] Page Agent loaded - ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
// ==UserScript==
|
||||
// @name PWA Everything
|
||||
// @author Evan Reichard
|
||||
// @version 0.0.1
|
||||
// @match *://*/*
|
||||
// @grant none
|
||||
// @run-at document-idle
|
||||
// @noframes
|
||||
// ==/UserScript==
|
||||
|
||||
let webManifest = {
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"theme_color": "#ff0000",
|
||||
"background_color": "#ff0000",
|
||||
"display": "standalone"
|
||||
};
|
||||
|
||||
let manifestElem = document.createElement('link');
|
||||
manifestElem.setAttribute('rel', 'manifest');
|
||||
manifestElem.setAttribute('href', 'data:application/manifest+json;base64,' + btoa(JSON.stringify(webManifest)));
|
||||
document.head.prepend(manifestElem);
|
||||
@@ -0,0 +1,229 @@
|
||||
// ==UserScript==
|
||||
// @name XenOrchestra NG Theme
|
||||
// @match https://10.0.50.100/*
|
||||
// @grant none
|
||||
// @version 0.1
|
||||
// @author Evan Reichard
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const css = `
|
||||
/* ------------------------------------- */
|
||||
/* ---------- Menu & Sub-Menu ---------- */
|
||||
/* ------------------------------------- */
|
||||
|
||||
.xo-menu, .xo-sub-menu {
|
||||
background-color: #111225;
|
||||
padding: 5px;
|
||||
border: 1px solid #363647;
|
||||
}
|
||||
|
||||
.xo-menu + div > div {
|
||||
overflow: unset !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
text-transform: uppercase;
|
||||
border-top: 1px solid #363647;
|
||||
border-bottom: 0px;
|
||||
float: left;
|
||||
min-width: max-content;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-tabs a.nav-link,
|
||||
.nav-tabs a.nav-link:focus,
|
||||
.nav-tabs a.nav-link:hover,
|
||||
.nav-tabs a.nav-link.active,
|
||||
.nav-tabs a.nav-link.active:focus,
|
||||
.nav-tabs a.nav-link.active:hover {
|
||||
color: #f8f8f8;
|
||||
border-bottom: 2px solid #8f84ff;
|
||||
}
|
||||
|
||||
.nav-tabs a.nav-link:hover {
|
||||
background-color: #393566;
|
||||
}
|
||||
|
||||
.nav-tabs a.nav-link {
|
||||
border-radius: 0px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item + .nav-item {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.xo-menu-item:hover, .nav-link.active {
|
||||
background-color: #2b284d !important;
|
||||
}
|
||||
|
||||
.xo-menu-item, .nav-link.active {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.nav-link.active:hover {
|
||||
background-color: #393566 !important;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------- */
|
||||
/* ------------ Global Style ----------- */
|
||||
/* ------------------------------------- */
|
||||
|
||||
/* HTML Text Color */
|
||||
table, .form-group, .ct-label, .ct-point, h2, #newSrForm, .table, h3 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* SVG Text Color */
|
||||
text {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom: 2px solid #363647;
|
||||
}
|
||||
|
||||
.thead-default th {
|
||||
color: #e0e0e0;
|
||||
background-color: #1e1e2d;
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
border-top: 1px solid #363647;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: white;
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
.card, .list-group-item, tr {
|
||||
background-color: black;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ------------------------------------- */
|
||||
/* ------------ Page Header ------------ */
|
||||
/* ------------------------------------- */
|
||||
|
||||
.page-header {
|
||||
padding: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.page-header .container-fluid {
|
||||
padding: 0px !important;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.page-header > div > div:first-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.header-title + div {
|
||||
text-align: unset !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
/* Header Tabs Horizontal Scroll */
|
||||
.page-header > div > div:first-child > div:has(.pull-right) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.page-header > .container-fluid > div:not([class*="_itemRowHeader"]):first-child > div:first-child {
|
||||
margin: 10px !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------- */
|
||||
/* -------------- VM Page -------------- */
|
||||
/* ------------------------------------- */
|
||||
|
||||
/* VM List Buttons */
|
||||
.page-header > .container-fluid > [class*="_itemRowHeader"].row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header > .container-fluid > [class*="_itemRowHeader"].row:nth-child(2) > .hidden-sm-down {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Expanded VM Graphs */
|
||||
.row > [class*="_itemExpanded"]:has(.row > [class*="_itemExpanded"]) {
|
||||
/* width: 100%; */
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* VM Page Start / Stop Buttons */
|
||||
.row:has(.header-title) > div:nth-child(2):has(div > .btn-group) {
|
||||
margin: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.row > div {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
div:has(> .nav.nav-tabs) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
nav + div {
|
||||
overflow-y: unset !important;
|
||||
background-color: black;
|
||||
padding: 10px !important;
|
||||
border: 1px solid #363647;
|
||||
}
|
||||
|
||||
.container-fluid, [class$="_itemContainer"], #vmCreation, #networkCreation {
|
||||
background-color: #111225;
|
||||
color: white;
|
||||
padding: 20px !important;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #363647;
|
||||
}
|
||||
|
||||
.container-fluid > .container-fluid {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
[class$="_itemContainer"] [class$="_item"] {
|
||||
border-bottom: 1px solid #363647;
|
||||
}
|
||||
|
||||
[class$="_itemContainer"] [class$="_item"]:hover {
|
||||
background-color: #393566 !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------- */
|
||||
/* ------------- VM Console ------------ */
|
||||
/* ------------------------------------- */
|
||||
|
||||
div:has(input[value="1"]) ~ .console canvas {
|
||||
width: 100% !important;
|
||||
height: unset !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
// ==UserScript==
|
||||
// @name Yattee Redirect
|
||||
// @version 0.0.3
|
||||
// @updateURL https://gist.github.com/evanreichard/8db561c7be7a6c308db18ce3e983c8d6/raw/yattee-redirect.user.js
|
||||
// @match https://www.youtube.com/*
|
||||
// @match https://m.youtube.com/*
|
||||
// @grant none
|
||||
// @noframes
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
|
||||
function attemptRedirect(){
|
||||
if (document.location.pathname == "/watch") {
|
||||
let newPath = document.location.href.split("https://" + document.location.host + "/")[1];
|
||||
document.location.href = "yattee://" + newPath
|
||||
}
|
||||
}
|
||||
|
||||
var pushState = history.pushState;
|
||||
history.pushState = function () {
|
||||
pushState.apply(history, arguments);
|
||||
attemptRedirect();
|
||||
};
|
||||
|
||||
attemptRedirect();
|
||||
Reference in New Issue
Block a user