您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Resize card list, add margin, overlays for video/text-only posts, and hide text-only toggle
// ==UserScript== // @name Coomer Card Resize + Labels // @namespace Violentmonkey Scripts // @match https://coomer.st/* // @version 1.6 // @author Nimby345 // @description Resize card list, add margin, overlays for video/text-only posts, and hide text-only toggle // @grant none // ==/UserScript== (function () { "use strict"; // ---------- Settings ---------- const defaultColumns = 5; // initial cards per row const cardGapEm = 0.25; // gap between cards let hideEmptyPosts = false; // Regex to match allowed URLs (creator pages + /posts) const urlPattern = /^https:\/\/coomer\.st\/(?:fansly|onlyfans)\/user\/[^/?#]+(?:\?o=\d+)?$|^https:\/\/coomer\.st\/posts(?:[/?#].*)?$/; // ---------- Styling ---------- function injectStyles() { if (document.getElementById("ccr-styles")) return; const css = ` .ccr-controls { position: fixed; z-index: 99999; right: 14px; bottom: 14px; display: flex; flex-direction: column; gap: 8px; background: rgba(20,20,20,.9); border: 1px solid rgba(255,255,255,.12); border-radius: 12px; padding: 10px 12px; backdrop-filter: blur(6px); font: 13px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; color: #eee; } .ccr-controls input[type="range"]{ width: 140px; } .ccr-controls label { display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; } .ccr-chip { padding: 2px 6px; border-radius: 999px; background: rgba(255,255,255,.15); font-weight: 600; min-width: 2ch; text-align:center; } .ccr-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 3px 6px; border-radius: 6px; background: #000; /* fully opaque */ color: #fff; font-size: 12px; font-weight: 600; pointer-events: none; } .ccr-badge-video { background: rgba(25, 118, 210, .8); } .ccr-badge-text { background: rgba(158, 158, 158, .85); } `; const style = document.createElement("style"); style.id = "ccr-styles"; style.textContent = css; document.head.appendChild(style); } // ---------- Controls ---------- function buildControls() { if (document.getElementById("ccr-controls")) return; const controls = document.createElement("div"); controls.id = "ccr-controls"; controls.className = "ccr-controls"; controls.innerHTML = ` <div style="display:flex;align-items:center;gap:6px;"> <span>Columns</span> <input id="ccr-col-slider" type="range" min="2" max="8" step="1" value="${defaultColumns}"> <span class="ccr-chip" id="ccr-col-readout">${defaultColumns}</span> </div> <label title="Hide posts that contain no images or videos"> <input id="ccr-hide-textonly" type="checkbox"> Hide text-only posts </label> `; document.body.appendChild(controls); const slider = controls.querySelector("#ccr-col-slider"); const readout = controls.querySelector("#ccr-col-readout"); const toggle = controls.querySelector("#ccr-hide-textonly"); slider.addEventListener("input", () => { const n = parseInt(slider.value, 10); readout.textContent = String(n); setColumns(n); }); toggle.addEventListener("change", () => { hideEmptyPosts = toggle.checked; labelAndFilterPosts(); }); } function setColumns(n) { const list = document.querySelector(".card-list__items"); if (!list) return; const listWidth = list.clientWidth || list.getBoundingClientRect().width || 1200; const px = Math.max(160, Math.floor(listWidth / n)); list.style.setProperty("--card-size", px + "px", "important"); list.style.gap = cardGapEm + "em"; } // ---------- Overlay + Filtering ---------- function labelAndFilterPosts() { const cards = document.querySelectorAll(".card-list__items > *"); cards.forEach((card) => { if (card.dataset.ccrLabeled) return; card.dataset.ccrLabeled = "true"; const hasVideo = !!card.querySelector("video"); const hasImage = !!card.querySelector("img"); let labelText = ""; let kind = ""; if (hasVideo) { labelText = "Post Contains Video"; kind = "video"; } else if (!hasImage) { labelText = "Text Only Post"; kind = "text"; } if (labelText) { const label = document.createElement("div"); label.textContent = labelText; label.className = "ccr-overlay " + (kind === "video" ? "ccr-badge-video" : "ccr-badge-text"); card.style.position = "relative"; card.appendChild(label); } // mark text-only posts if (!hasVideo && !hasImage) { card.classList.add("ccr-text-only"); } }); // Apply toggle document.querySelectorAll(".card-list__items > *.ccr-text-only").forEach((c) => { c.style.display = hideEmptyPosts ? "none" : ""; }); } // ---------- Mutation observer ---------- let observer; function observeChanges() { if (observer) observer.disconnect(); observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === "childList" && m.addedNodes.length) { setColumns(getCurrentColumns()); labelAndFilterPosts(); } } }); observer.observe(document.body, { childList: true, subtree: true }); } function getCurrentColumns() { const el = document.getElementById("ccr-col-slider"); return el ? parseInt(el.value, 10) : defaultColumns; } // ---------- Init ---------- function shouldRun() { return urlPattern.test(location.href); } function init() { if (!shouldRun()) return; injectStyles(); buildControls(); setColumns(getCurrentColumns()); labelAndFilterPosts(); observeChanges(); } window.addEventListener("popstate", init); window.addEventListener("load", () => { init(); setInterval(() => { if (!shouldRun()) return; setColumns(getCurrentColumns()); labelAndFilterPosts(); }, 1000); }); })();