Streamlined filter bar, instant client search & filtering, ad cleaner, and seamless auto next page infinite scroll for Rule34Video.
// ==UserScript==
// @name Better Rule34Video
// @namespace https://github.com/quantavil/userscript/
// @version 1.5.0
// @author quantavil
// @description Streamlined filter bar, instant client search & filtering, ad cleaner, and seamless auto next page infinite scroll for Rule34Video.
// @license MIT
// @match *://*.rule34video.com/*
// @match *://rule34video.com/*
// @run-at document-end
// ==/UserScript==
(function() {
"use strict";
var NON_LISTING_SEGMENTS = [...[
"tags",
"categories",
"models",
"channels",
"playlists"
], "video"];
function isNonListingSegment(segment) {
return NON_LISTING_SEGMENTS.includes(segment);
}
function isPaginationKey(key) {
return /^from/i.test(key) || /^page$/i.test(key) || /^p$/i.test(key);
}
function stripPageSegment(pathname) {
if (!pathname || pathname === "/") return "/";
if (/^\/(?:tags|categories|models|channels|playlists)\/[^/]+\/\d+\/?$/.exec(pathname)) return pathname.replace(/\/\d+\/?$/, "/");
const catalogMatch = /^\/([^/]+)\/\d+\/?$/.exec(pathname);
if (catalogMatch && !isNonListingSegment(catalogMatch[1])) return `/${catalogMatch[1]}/`;
return pathname;
}
function pageNumberFromPath(pathname) {
const entityMatch = /^\/(?:tags|categories|models|channels|playlists)\/[^/]+\/(\d+)\/?$/.exec(pathname);
if (entityMatch) {
const p = parseInt(entityMatch[1], 10);
if (!isNaN(p) && p > 0) return p;
}
const catalogMatch = /^\/([^/]+)\/(\d+)\/?$/.exec(pathname);
if (catalogMatch && !isNonListingSegment(catalogMatch[1])) {
const p = parseInt(catalogMatch[2], 10);
if (!isNaN(p) && p > 0) return p;
}
return null;
}
function appendPageToPath(pathname, pageNum) {
if (!pathname || pathname === "/") return `/latest-updates/${pageNum}/`;
const entityMatch = /^(\/(?:tags|categories|models|channels|playlists)\/[^/]+)(?:\/\d+)?\/?$/.exec(pathname);
if (entityMatch) return `${entityMatch[1]}/${pageNum}/`;
const catalogMatch = /^(\/[^/]+)(?:\/\d+)?\/?$/.exec(pathname);
if (catalogMatch && !isNonListingSegment(catalogMatch[1].slice(1))) return `${catalogMatch[1]}/${pageNum}/`;
if (/\/\d+\/?$/.test(pathname)) return pathname.replace(/\/\d+\/?$/, `/${pageNum}/`);
if (pathname.endsWith("/")) return `${pathname}${pageNum}/`;
return `${pathname}/${pageNum}/`;
}
var BASE_YEAR = 2018;
var DEFAULT_FILTER = {
query: "",
soundOnly: false,
hdOnly: false,
futaFilter: "all",
hideWatched: false,
minRating: 0,
minViews: 0,
minYear: BASE_YEAR,
durationMinSeconds: null
};
function parseDuration(timeStr) {
if (!timeStr) return 0;
const cleaned = timeStr.trim().replace(/[^\d:]/g, "");
if (!cleaned) return 0;
const parts = cleaned.split(":").map((p) => parseInt(p, 10));
if (parts.some(isNaN)) return 0;
if (parts.length === 1) return parts[0];
if (parts.length === 2) return parts[0] * 60 + parts[1];
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
return 0;
}
function parseViews(viewStr) {
if (!viewStr) return 0;
const match = /([\d.]+)\s*([KkMmBb])?/.exec(viewStr.replace(/,/g, "").trim());
if (!match) return 0;
const num = parseFloat(match[1]);
if (isNaN(num)) return 0;
const unit = match[2]?.toUpperCase();
if (unit === "K") return Math.round(num * 1e3);
if (unit === "M") return Math.round(num * 1e6);
if (unit === "B") return Math.round(num * 1e9);
return Math.round(num);
}
function parseRating(ratingStr) {
if (!ratingStr) return {
percent: 0,
count: 0
};
const percentMatch = /(\d+)%/.exec(ratingStr);
const countMatch = /\(([\d,]+)\)/.exec(ratingStr);
return {
percent: percentMatch ? parseInt(percentMatch[1], 10) : 0,
count: countMatch ? parseInt(countMatch[1].replace(/,/g, ""), 10) : 0
};
}
function parseSubmittedYear(dateStr, currentYear = new Date().getFullYear()) {
if (!dateStr) return currentYear;
const text = dateStr.toLowerCase().trim();
const explicitYearMatch = /\b(20[12]\d)\b/.exec(text);
if (explicitYearMatch) return parseInt(explicitYearMatch[1], 10);
const yearsAgoMatch = /(\d+)\s+years?\s+ago/.exec(text);
if (yearsAgoMatch) {
const diff = parseInt(yearsAgoMatch[1], 10);
return Math.max(BASE_YEAR, currentYear - diff);
}
const monthsAgoMatch = /(\d+)\s+months?\s+ago/.exec(text);
if (monthsAgoMatch) {
const months = parseInt(monthsAgoMatch[1], 10);
const d = new Date();
d.setMonth(d.getMonth() - months);
return d.getFullYear();
}
return currentYear;
}
function isAdCard(el) {
if (el.classList.contains("spot-thumb") || el.closest(".spots")) return true;
if (el.querySelector("iframe")) return true;
const header = el.querySelector("header");
if (header && /AD/i.test(header.textContent ?? "")) return true;
const link = el.querySelector("a.th, a");
if (link) {
const href = link.getAttribute("href") || "";
if (href.includes("/v1/d.php") || href.includes("sadbaguette") || href.includes("ku34bh9la09")) return true;
}
const hasVideoCardId = Boolean(el.dataset.videoCardId);
const hasVideoHref = Boolean(el.querySelector("a[href*=\"/video/\"]"));
return !hasVideoCardId && !hasVideoHref;
}
function extractCardData(el) {
if (isAdCard(el)) return null;
const cardLink = el.querySelector("a.th, a[href*=\"/video/\"]");
const href = cardLink?.getAttribute("href") || "";
const id = el.dataset.videoCardId || videoIdFromHref(href) || "";
if (!id || !href) return null;
const title = el.querySelector(".thumb_title")?.textContent?.trim() || cardLink?.getAttribute("title")?.trim() || "";
const imgEl = el.querySelector("img.thumb, img");
const thumbUrl = imgEl?.getAttribute("data-webp") || imgEl?.getAttribute("data-original") || imgEl?.src || "";
const previewWrap = el.querySelector(".wrap_image");
const previewUrl = previewWrap?.dataset.preview || previewWrap?.getAttribute("data-preview") || null;
const durationFormatted = el.querySelector(".time")?.textContent?.trim() || "";
const durationSeconds = parseDuration(durationFormatted);
const { percent: ratingPercent, count: votesCount } = parseRating((el.querySelector(".video-card-meta__rating") || el.querySelector(".rating"))?.textContent ?? "");
const viewsFormatted = (el.querySelector(".video-views-count") || el.querySelector(".views"))?.textContent?.trim() || "";
const viewsCount = parseViews(viewsFormatted);
const commentsEl = el.querySelector(".video-comments-count");
const commentsCount = parseInt(commentsEl?.textContent?.trim() || "0", 10) || 0;
const hasSound = el.querySelector(".sound") !== null;
const isHd = el.querySelector(".quality") !== null || el.querySelector(".custom-hd") !== null;
const isFuta = el.querySelector(".futa") !== null;
const isWatched = previewWrap?.classList.contains("watched") || el.classList.contains("watched") || Boolean(el.querySelector(".watched"));
const dateEl = el.querySelector(".video-card-meta__date") || el.querySelector(".added");
const submittedAgo = dateEl?.getAttribute("title") || dateEl?.textContent?.trim() || "";
const submittedYear = parseSubmittedYear(submittedAgo);
return {
id,
title,
url: href.startsWith("http") ? href : new URL(href, "https://rule34video.com").href,
previewUrl,
thumbUrl,
durationSeconds,
durationFormatted,
ratingPercent,
votesCount,
viewsCount,
viewsFormatted,
commentsCount,
hasSound,
isHd,
isFuta,
isWatched,
submittedAgo,
submittedYear
};
}
function videoIdFromHref(href) {
if (!href) return "";
return /video\/(\d+)/.exec(href)?.[1] || "";
}
function viewsToNearestStep(views, steps) {
if (!steps.length) return 0;
let best = 0;
let bestDist = Math.abs(views - steps[0]);
for (let i = 1; i < steps.length; i++) {
const d = Math.abs(views - steps[i]);
if (d < bestDist) {
bestDist = d;
best = i;
}
}
return best;
}
function matchesClientFilter(card, filter) {
if (filter.query.trim()) {
const keywords = filter.query.toLowerCase().trim().split(/\s+/);
const titleLower = card.title.toLowerCase();
for (const kw of keywords) if (!titleLower.includes(kw)) return false;
}
if (filter.soundOnly && !card.hasSound) return false;
if (filter.hdOnly && !card.isHd) return false;
if (filter.futaFilter === "hide" && card.isFuta) return false;
if (filter.futaFilter === "only" && !card.isFuta) return false;
if (filter.hideWatched && card.isWatched) return false;
if (filter.minRating > 0 && card.ratingPercent < filter.minRating) return false;
if (filter.minViews > 0 && card.viewsCount < filter.minViews) return false;
if (filter.minYear > 2018 && card.submittedYear < filter.minYear) return false;
if (filter.durationMinSeconds !== null && card.durationSeconds < filter.durationMinSeconds) return false;
return true;
}
function resolveNextPageUrl(currentUrlStr, nextRawHref) {
try {
const trimmedHref = nextRawHref.trim();
if (!trimmedHref || trimmedHref.startsWith("#") || trimmedHref.startsWith("javascript:")) return "";
const current = new URL(currentUrlStr);
const next = new URL(trimmedHref, current.href);
if (!next.search && current.search) {
const carried = new URLSearchParams();
current.searchParams.forEach((val, key) => {
if (!isPaginationKey(key)) carried.append(key, val);
});
next.search = carried.toString();
} else if (current.search && next.search) current.searchParams.forEach((val, key) => {
if (!isPaginationKey(key) && !next.searchParams.has(key)) next.searchParams.set(key, val);
});
return next.toString();
} catch {
return nextRawHref;
}
}
function parseCurrentUrlFilters(urlStr) {
try {
const params = new URL(urlStr).searchParams;
const result = {};
const fromDate = params.get("post_date_from");
if (fromDate) {
const yrMatch = /^(\d{4})/.exec(fromDate);
if (yrMatch) result.minYear = Math.max(BASE_YEAR, parseInt(yrMatch[1], 10));
}
const fromDur = params.get("duration_from");
if (fromDur && !isNaN(parseInt(fromDur, 10))) result.durationMinSeconds = parseInt(fromDur, 10);
return result;
} catch {
return {};
}
}
function parseKvsParameters(dataParams) {
const raw = {};
if (!dataParams) return {
fromParam: null,
sortBy: null,
query: null,
tagIds: null,
raw
};
const parts = dataParams.split(";");
for (const part of parts) {
const colonIdx = part.indexOf(":");
if (colonIdx !== -1) {
const key = part.slice(0, colonIdx).trim();
const val = part.slice(colonIdx + 1).trim();
if (key) raw[key] = val;
}
}
let fromParam = null;
for (const k of Object.keys(raw)) if (/(?:from_videos(?:\+| )from_albums|from_videos|from_albums|from)/i.test(k)) {
const parsed = parseInt(raw[k], 10);
if (!isNaN(parsed) && parsed > 0) {
fromParam = parsed;
break;
}
}
const sortBy = raw["sort_by"] !== void 0 && raw["sort_by"] !== "" ? raw["sort_by"] : null;
const query = raw["q"] !== void 0 && raw["q"] !== "" ? raw["q"] : null;
const tagIds = raw["tag_ids"] !== void 0 && raw["tag_ids"] !== "" ? raw["tag_ids"] : null;
return {
fromParam,
sortBy,
query,
tagIds,
raw
};
}
var AD_SELECTORS = [
".spot-thumb",
".spots",
".sidebar_ad_buttons",
".footer_spots",
"ins.adsbyjuicy",
"iframe[src*=\"sadbaguette\"]",
"iframe[src*=\"traffic\"]",
"iframe[src*=\"ads\"]",
"iframe[src*=\"adserver\"]",
"iframe[src*=\"/ads/\"]",
"iframe[src*=\"juicy\"]",
".item.thumb:has(header)",
".item.thumb:has(iframe)",
".item.thumb a[href*=\"/v1/d.php\"]"
];
function cleanAds(root = document) {
for (const selector of AD_SELECTORS) try {
const elements = root.querySelectorAll(selector);
for (const el of elements) (el.closest(".item.thumb") ?? el).remove();
} catch {}
const thumbs = root.querySelectorAll(".item.thumb");
for (const thumb of thumbs) if (isAdCard(thumb)) thumb.remove();
}
var WATCHED_KEY = "better_rule34_watched_v1";
var MAX_WATCHED = 2e3;
function readWatchedIds() {
try {
const raw = localStorage.getItem(WATCHED_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.filter((v) => typeof v === "string");
} catch {
return [];
}
}
var cachedIds = null;
function getWatchedIds() {
if (!cachedIds) cachedIds = new Set(readWatchedIds());
return cachedIds;
}
function isWatchedId(id) {
if (!id) return false;
return getWatchedIds().has(id);
}
function markWatched(id) {
if (!id) return;
try {
const ids = readWatchedIds().filter((v) => v !== id);
ids.push(id);
while (ids.length > MAX_WATCHED) ids.shift();
localStorage.setItem(WATCHED_KEY, JSON.stringify(ids));
if (cachedIds) {
cachedIds.delete(id);
cachedIds.add(id);
while (cachedIds.size > MAX_WATCHED) {
const oldest = cachedIds.values().next();
if (oldest.done) break;
cachedIds.delete(oldest.value);
}
}
} catch {}
}
function findVideoAnchor(from) {
if (!from) return null;
const anchor = from.closest?.("a[href*=\"/video/\"]");
if (!(anchor instanceof HTMLAnchorElement)) return null;
if (!/\/video\//.test(anchor.getAttribute("href") || "")) return null;
return anchor;
}
function hardenAnchor(a) {
if (a.target !== "_blank") a.target = "_blank";
const rel = (a.getAttribute("rel") || "").toLowerCase();
if (!rel.includes("noopener")) a.setAttribute("rel", (rel ? `${rel} ` : "") + "noopener");
}
function hardenAnchorsIn(root) {
const anchors = root.querySelectorAll("a[href*=\"/video/\"]");
for (const a of anchors) hardenAnchor(a);
}
function shouldNewTabClick(e) {
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey;
}
var newTabWired = false;
function initNewTab(scope = document) {
hardenAnchorsIn(scope);
if (newTabWired) return () => {};
newTabWired = true;
const onClick = (e) => {
const target = e.target;
if (!target || !(target instanceof Element)) return;
const anchor = findVideoAnchor(target);
if (!anchor) return;
hardenAnchor(anchor);
if (!shouldNewTabClick(e)) return;
const href = anchor.getAttribute("href") || anchor.href;
if (!href) return;
e.preventDefault();
e.stopPropagation();
markWatched(videoIdFromHref(href));
window.open(anchor.href, "_blank", "noopener");
};
const onAuxClick = (e) => {
if (e.button !== 1) return;
const target = e.target;
if (!target || !(target instanceof Element)) return;
const anchor = findVideoAnchor(target);
if (!anchor) return;
const href = anchor.getAttribute("href") || anchor.href;
if (!href) return;
markWatched(videoIdFromHref(href));
};
document.addEventListener("click", onClick, true);
document.addEventListener("auxclick", onAuxClick, true);
return () => {
document.removeEventListener("click", onClick, true);
document.removeEventListener("auxclick", onAuxClick, true);
};
}
function isListingPage(urlStr = window.location.href) {
try {
const url = new URL(urlStr);
if (/\/video(\/|$)/.test(url.pathname)) return false;
if (/^\/(login|signup|invite|premium|static|info|feedback)/.test(url.pathname)) return false;
return true;
} catch {
return true;
}
}
function findVideosContainer() {
return document.querySelector(".content_general .thumbs") || document.querySelector("[id^=\"custom_list_videos_\"][id$=\"_items\"].thumbs") || document.querySelector(".twocolumns .thumbs") || document.querySelector(".thumbs") || document.querySelector("[id$=\"_items\"]");
}
function unclipBodyOverflow() {
const wrappers = document.querySelectorAll("body > div");
for (const w of wrappers) if (w.style.overflow === "hidden") w.style.overflow = "visible";
}
function readActiveSort(root = document) {
const activeBtn = root.querySelector(".filters-panel__section--sort .btn.active, .filters-panel .btn.active[data-parameters*=\"sort_by\"]");
if (activeBtn) {
const dataParams = activeBtn.getAttribute("data-parameters") || "";
const match = /(?:^|;)sort_by:([^;]*)(?:;|$)/.exec(dataParams);
if (match && match[1]) return match[1];
}
return null;
}
function computeNextPageUrl(currentUrlStr, nextPageNum, sortBy) {
try {
const url = new URL(currentUrlStr);
const pathname = url.pathname;
if (sortBy !== void 0) {
if (sortBy) url.searchParams.set("sort_by", sortBy);
else url.searchParams.delete("sort_by");
}
if (pathname.includes("/search/")) {
url.searchParams.set("from_videos", String(nextPageNum));
url.searchParams.delete("from_videos+from_albums");
url.searchParams.delete("from_videos from_albums");
return url.toString();
}
url.pathname = appendPageToPath(pathname, nextPageNum);
for (const k of [...url.searchParams.keys()]) if (isPaginationKey(k)) url.searchParams.delete(k);
return url.toString();
} catch {
return null;
}
}
function parseNextLink(root, baseUrl, fallbackSortBy) {
const nextLink = root.querySelector(".pagination .item.pager.next a, .pagination .item.active + .item a, .pagination a.next");
if (!nextLink) return {
url: null,
fromParam: null,
sortBy: null
};
const raw = nextLink.getAttribute("href") || "";
const dataParams = nextLink.getAttribute("data-parameters") || "";
if (raw && !raw.startsWith("#") && !raw.startsWith("javascript:")) return {
url: resolveNextPageUrl(baseUrl, raw) || null,
fromParam: null,
sortBy: null
};
if (dataParams) {
const parsed = parseKvsParameters(dataParams);
const effectiveSort = parsed.sortBy !== null ? parsed.sortBy : fallbackSortBy ?? null;
if (parsed.fromParam !== null && !isNaN(parsed.fromParam)) return {
url: computeNextPageUrl(baseUrl, parsed.fromParam, effectiveSort),
fromParam: parsed.fromParam,
sortBy: effectiveSort
};
}
return {
url: null,
fromParam: null,
sortBy: null
};
}
var AutoPager = class {
container = null;
statusContainer = null;
sentinel = null;
observer = null;
nextUrl = null;
currentPage = 1;
initialPage = 1;
lastPageUrl = window.location.href;
isLoading = false;
isAppending = false;
seenCardIds = new Set();
onNewCards;
onPageLoaded;
scrollHandler = null;
constructor(options) {
this.onNewCards = options.onNewCards;
this.onPageLoaded = options.onPageLoaded;
}
init() {
unclipBodyOverflow();
if (!isListingPage()) return;
this.container = findVideosContainer();
if (!this.container) return;
this.currentPage = this.detectCurrentPageNumber();
this.initialPage = this.currentPage;
this.lastPageUrl = window.location.href;
const initialCards = this.container.querySelectorAll(".item.thumb");
for (const card of initialCards) {
if (isAdCard(card)) {
card.remove();
continue;
}
const id = card.dataset.videoCardId || card.querySelector("a[href*=\"/video/\"]")?.getAttribute("href");
if (id) this.seenCardIds.add(id);
}
this.detectNextPageUrl(document);
this.mountStatusElements();
this.setupObserver();
this.setupScrollListener();
}
getIsAppending() {
return this.isAppending;
}
getNextUrl() {
return this.nextUrl;
}
getCurrentPage() {
return this.currentPage;
}
getTotalLoadedCount() {
return this.seenCardIds.size;
}
getPagesLoaded() {
return Math.max(1, this.currentPage - this.initialPage + 1);
}
getCurrentPageUrl() {
return this.lastPageUrl;
}
detectCurrentPageNumber() {
try {
const url = new URL(window.location.href);
const fromParam = url.searchParams.get("from_videos") || url.searchParams.get("from_videos+from_albums") || url.searchParams.get("from_videos from_albums") || url.searchParams.get("from") || url.searchParams.get("page") || url.searchParams.get("p");
if (fromParam) {
const p = parseInt(fromParam, 10);
if (!isNaN(p) && p > 0) return p;
}
const pageFromPath = pageNumberFromPath(url.pathname);
if (pageFromPath !== null) return pageFromPath;
} catch {}
return 1;
}
reset(nextUrl, newPageNumber = 1) {
if (this.container) for (const sep of this.container.querySelectorAll(".br34-page-sep")) sep.remove();
this.seenCardIds.clear();
this.container = findVideosContainer();
if (this.container) {
const currentCards = this.container.querySelectorAll(".item.thumb");
for (const card of currentCards) {
if (isAdCard(card)) {
card.remove();
continue;
}
const id = card.dataset.videoCardId || card.querySelector("a[href*=\"/video/\"]")?.getAttribute("href");
if (id) this.seenCardIds.add(id);
}
}
this.currentPage = newPageNumber;
this.initialPage = newPageNumber;
this.lastPageUrl = window.location.href;
this.isLoading = false;
this.isAppending = false;
if (nextUrl !== void 0) this.nextUrl = nextUrl;
else this.detectNextPageUrl(document);
this.mountStatusElements();
this.setupObserver();
}
detectNextPageUrl(root) {
const activeSort = readActiveSort(root);
const { url, fromParam, sortBy } = parseNextLink(root, window.location.href, activeSort);
this.nextUrl = null;
if (url) this.nextUrl = url;
else if (fromParam !== null && !isNaN(fromParam)) this.nextUrl = computeNextPageUrl(window.location.href, fromParam, sortBy || activeSort);
if (!this.nextUrl) try {
if (!new URL(window.location.href).pathname.includes("/search/")) this.nextUrl = computeNextPageUrl(window.location.href, this.currentPage + 1, activeSort);
} catch {
this.nextUrl = null;
}
const nativePagination = document.querySelector(".pagination");
if (nativePagination) nativePagination.style.display = "none";
}
mountStatusElements() {
if (!this.container) return;
if (!this.statusContainer) {
this.statusContainer = document.createElement("div");
this.statusContainer.className = "br34-autopager-container";
this.container.after(this.statusContainer);
}
if (!this.sentinel) {
this.sentinel = document.createElement("div");
this.sentinel.className = "br34-sentinel";
this.sentinel.style.height = "1px";
this.statusContainer.before(this.sentinel);
}
this.updateStatusDisplay();
}
setupObserver() {
if (typeof IntersectionObserver === "undefined") return;
this.observer?.disconnect();
this.observer = new IntersectionObserver((entries) => {
if (this.isLoading || !this.nextUrl) return;
for (const entry of entries) if (entry.isIntersecting) {
this.loadNextPage();
break;
}
}, {
rootMargin: "1000px 0px",
threshold: 0
});
if (this.sentinel) this.observer.observe(this.sentinel);
}
setupScrollListener() {
if (this.scrollHandler) return;
let ticking = false;
this.scrollHandler = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
if (this.isLoading || !this.nextUrl) return;
const scrollY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
const winHeight = window.innerHeight || document.documentElement.clientHeight;
const docHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight);
if (scrollY + winHeight >= docHeight - 1200) this.loadNextPage();
});
};
window.addEventListener("scroll", this.scrollHandler, { passive: true });
window.addEventListener("touchmove", this.scrollHandler, { passive: true });
window.addEventListener("resize", this.scrollHandler, { passive: true });
}
updateStatusDisplay() {
if (!this.statusContainer) return;
if (this.isLoading) this.statusContainer.innerHTML = `
<div class="br34-autopager-loading">
<div class="br34-spinner"></div>
<span>ACQUIRING SECTOR // PAGE ${this.currentPage + 1}...</span>
</div>
`;
else if (!this.nextUrl) this.statusContainer.innerHTML = `
<div class="br34-autopager-end">
[ ARCHIVE EXHAUSTED // ${this.seenCardIds.size} UNITS INDEXED ]
</div>
`;
else this.statusContainer.innerHTML = "";
}
async loadNextPage() {
if (this.isLoading || !this.nextUrl) return;
this.container = findVideosContainer();
if (!this.container) return;
this.isLoading = true;
this.updateStatusDisplay();
const fetchUrl = this.nextUrl;
let hasError = false;
try {
const response = await fetch(fetchUrl, {
credentials: "include",
headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" }
});
if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${fetchUrl}`);
const htmlText = await response.text();
const doc = new DOMParser().parseFromString(htmlText, "text/html");
cleanAds(doc);
const newCardElements = Array.from(doc.querySelectorAll(".thumbs .item.thumb, .item.thumb"));
const cardsToAppend = [];
for (const card of newCardElements) {
if (isAdCard(card)) continue;
const anchor = card.querySelector("a[href*=\"/video/\"]");
const href = anchor?.getAttribute("href") || "";
const id = card.dataset.videoCardId || href;
if (id && this.seenCardIds.has(id)) continue;
if (id) this.seenCardIds.add(id);
if (anchor) hardenAnchor(anchor);
const img = card.querySelector("img");
if (img) {
const webp = img.getAttribute("data-webp") || img.dataset.webp;
const original = img.getAttribute("data-original") || img.dataset.original;
const resolvedSrc = webp || original;
if (resolvedSrc) img.src = resolvedSrc;
img.removeAttribute("data-original");
img.removeAttribute("data-webp");
img.classList.remove("lazy-load");
img.loading = "lazy";
}
cardsToAppend.push(card);
}
if (cardsToAppend.length > 0) {
this.isAppending = true;
try {
const sep = document.createElement("div");
sep.className = "br34-page-sep";
sep.dataset.page = String(this.currentPage + 1);
sep.textContent = `[ PAGE ${this.currentPage + 1} ]`;
this.container.append(sep, ...cardsToAppend);
} finally {
setTimeout(() => {
this.isAppending = false;
}, 50);
}
this.onNewCards(cardsToAppend);
}
this.currentPage++;
this.lastPageUrl = fetchUrl;
this.onPageLoaded?.(this.currentPage);
const activeSort = readActiveSort(document);
const parsed = parseNextLink(doc, fetchUrl, activeSort);
this.nextUrl = null;
if (parsed.url) this.nextUrl = parsed.url;
else if (parsed.fromParam !== null && !isNaN(parsed.fromParam)) this.nextUrl = computeNextPageUrl(fetchUrl, parsed.fromParam, parsed.sortBy || activeSort);
if (!this.nextUrl && cardsToAppend.length > 0) try {
if (!new URL(fetchUrl).pathname.includes("/search/")) this.nextUrl = computeNextPageUrl(fetchUrl, this.currentPage + 1, activeSort);
} catch {
this.nextUrl = null;
}
} catch (err) {
hasError = true;
console.error("[Better Rule34] AutoPager error:", err);
if (this.statusContainer) {
this.statusContainer.innerHTML = `
<div class="br34-autopager-end" style="border-color: #ff0055; color: #ff0055;">
[ ERROR FETCHING SECTOR // <button type="button" class="br34-load-more-btn" style="padding: 4px 10px; font-size: 10px; margin-left: 6px;">RETRY</button> ]
</div>
`;
this.statusContainer.querySelector("button")?.addEventListener("click", () => {
this.loadNextPage();
});
}
} finally {
this.isLoading = false;
if (!hasError) this.updateStatusDisplay();
unclipBodyOverflow();
}
}
destroy() {
if (this.scrollHandler) {
window.removeEventListener("scroll", this.scrollHandler);
window.removeEventListener("touchmove", this.scrollHandler);
window.removeEventListener("resize", this.scrollHandler);
this.scrollHandler = null;
}
this.observer?.disconnect();
this.observer = null;
this.statusContainer?.remove();
this.sentinel?.remove();
this.statusContainer = null;
this.sentinel = null;
}
};
var BOOKMARK_ARCHIVE_KEY = "better_rule34_bookmarks_v2";
var LEGACY_BOOKMARK_KEY = "better_rule34_bookmarks_v1";
function canonicalListKey(urlStr) {
try {
const url = new URL(urlStr);
for (const k of [...url.searchParams.keys()]) if (isPaginationKey(k)) url.searchParams.delete(k);
let pathname = url.pathname;
if (!pathname || pathname === "/") pathname = "/latest-updates/";
else pathname = stripPageSegment(pathname);
if (pathname.length > 1 && !pathname.endsWith("/")) pathname += "/";
const qs = [...url.searchParams.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${k}=${v}`).join("&");
return qs ? `${pathname}?${qs}` : pathname;
} catch {
return urlStr;
}
}
function formatSectorTitleFromUrl(urlStr) {
try {
const url = new URL(urlStr);
const parts = url.pathname.split("/").filter(Boolean);
const sortBy = url.searchParams.get("sort_by");
const sortSuffix = sortBy ? ` [${sortBy.toUpperCase()}]` : "";
if (parts.length === 0 || parts.length === 1 && parts[0] === "latest-updates") return `FEED // LATEST${sortSuffix}`;
if (parts[0] === "search") {
const q = parts[1] || url.searchParams.get("q") || "ALL";
return `SEARCH // ${decodeURIComponent(q).toUpperCase()}${sortSuffix}`;
}
if (parts[0] === "tags" && parts[1]) return `TAG // ${decodeURIComponent(parts[1]).replace(/-/g, " ").toUpperCase()}${sortSuffix}`;
if (parts[0] === "categories" && parts[1]) return `CATEGORY // ${decodeURIComponent(parts[1]).replace(/-/g, " ").toUpperCase()}${sortSuffix}`;
if (parts[0] === "models" && parts[1]) return `MODEL // ${decodeURIComponent(parts[1]).replace(/-/g, " ").toUpperCase()}${sortSuffix}`;
if (parts[0] === "channels" && parts[1]) return `CHANNEL // ${decodeURIComponent(parts[1]).replace(/-/g, " ").toUpperCase()}${sortSuffix}`;
if (parts[0] === "playlists" && parts[1]) return `PLAYLIST // ${decodeURIComponent(parts[1]).replace(/-/g, " ").toUpperCase()}${sortSuffix}`;
return `SECTOR // ${parts.slice(0, 2).join(" / ").toUpperCase()}${sortSuffix}`;
} catch {
return "SECTOR // ARCHIVE";
}
}
function resolveStore$1(store) {
if (store) return store;
try {
if (typeof localStorage !== "undefined") return localStorage;
} catch {}
return null;
}
function readArchive(store) {
const s = resolveStore$1(store);
if (!s) return {
version: 2,
sectors: [],
videos: []
};
try {
const rawV2 = s.getItem(BOOKMARK_ARCHIVE_KEY);
if (rawV2) {
const parsed = JSON.parse(rawV2);
if (parsed && parsed.version === 2 && Array.isArray(parsed.sectors) && Array.isArray(parsed.videos)) return parsed;
}
const rawV1 = s.getItem(LEGACY_BOOKMARK_KEY);
if (rawV1) {
const legacy = JSON.parse(rawV1);
if (legacy && typeof legacy === "object") {
const sectors = [];
let i = 0;
for (const [key, val] of Object.entries(legacy)) if (val && typeof val.page === "number" && typeof val.url === "string") sectors.push({
id: `sec_legacy_${Date.now()}_${i++}`,
listKey: key,
title: formatSectorTitleFromUrl(val.url),
url: val.url,
page: val.page,
createdAt: Date.now()
});
const data = {
version: 2,
sectors,
videos: []
};
s.setItem(BOOKMARK_ARCHIVE_KEY, JSON.stringify(data));
return data;
}
}
} catch {}
return {
version: 2,
sectors: [],
videos: []
};
}
function writeArchive(data, store) {
const s = resolveStore$1(store);
if (!s) return;
try {
s.setItem(BOOKMARK_ARCHIVE_KEY, JSON.stringify(data));
} catch {}
}
function getSectors(store) {
return readArchive(store).sectors;
}
function findSector(listKey, page, store) {
if (!listKey) return null;
return getSectors(store).find((s) => s.listKey === listKey && s.page === page) || null;
}
function saveSector(bm, store) {
const archive = readArchive(store);
const existingIdx = archive.sectors.findIndex((s) => s.listKey === bm.listKey && s.page === bm.page);
const now = Date.now();
if (existingIdx !== -1) {
archive.sectors[existingIdx] = {
...archive.sectors[existingIdx],
...bm,
createdAt: now
};
writeArchive(archive, store);
return archive.sectors[existingIdx];
}
const created = {
...bm,
id: `sec_${now}_${Math.random().toString(36).slice(2, 7)}`,
createdAt: now
};
archive.sectors.unshift(created);
writeArchive(archive, store);
return created;
}
function deleteSector(id, store) {
if (!id) return;
const archive = readArchive(store);
archive.sectors = archive.sectors.filter((s) => s.id !== id);
writeArchive(archive, store);
}
function clearAllSectors(store) {
const archive = readArchive(store);
archive.sectors = [];
writeArchive(archive, store);
}
function getVideos(store) {
return readArchive(store).videos;
}
function isVideoSaved(id, store) {
if (!id) return false;
return getVideos(store).some((v) => v.id === id);
}
function saveVideo(video, store) {
const archive = readArchive(store);
const existingIdx = archive.videos.findIndex((v) => v.id === video.id);
const now = Date.now();
if (existingIdx !== -1) {
archive.videos[existingIdx] = {
...archive.videos[existingIdx],
...video,
createdAt: now
};
writeArchive(archive, store);
return archive.videos[existingIdx];
}
const created = {
...video,
createdAt: now
};
archive.videos.unshift(created);
writeArchive(archive, store);
return created;
}
function deleteVideo(id, store) {
if (!id) return;
const archive = readArchive(store);
archive.videos = archive.videos.filter((v) => v.id !== id);
writeArchive(archive, store);
}
function clearAllVideos(store) {
const archive = readArchive(store);
archive.videos = [];
writeArchive(archive, store);
}
function getTotalBookmarkCount(store) {
const archive = readArchive(store);
return archive.sectors.length + archive.videos.length;
}
function exportArchiveJson(store) {
const archive = readArchive(store);
return JSON.stringify(archive, null, 2);
}
function importArchiveJson(jsonStr, store) {
try {
const parsed = JSON.parse(jsonStr);
if (!parsed || typeof parsed !== "object") return {
success: false,
sectorsAdded: 0,
videosAdded: 0
};
const archive = readArchive(store);
let sectorsAdded = 0;
let videosAdded = 0;
if (Array.isArray(parsed.sectors)) {
for (const s of parsed.sectors) if (s && typeof s.listKey === "string" && typeof s.page === "number" && typeof s.url === "string") {
if (!archive.sectors.some((item) => item.listKey === s.listKey && item.page === s.page)) {
archive.sectors.push({
id: s.id || `sec_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
listKey: s.listKey,
title: s.title || formatSectorTitleFromUrl(s.url),
url: s.url,
page: s.page,
sortBy: s.sortBy || null,
createdAt: s.createdAt || Date.now()
});
sectorsAdded++;
}
}
}
if (Array.isArray(parsed.videos)) {
for (const v of parsed.videos) if (v && typeof v.id === "string" && typeof v.url === "string") {
if (!archive.videos.some((item) => item.id === v.id)) {
archive.videos.push({
id: v.id,
title: v.title || "Untitled Video",
url: v.url,
thumbUrl: v.thumbUrl || "",
durationFormatted: v.durationFormatted || "",
ratingPercent: v.ratingPercent || 0,
viewsFormatted: v.viewsFormatted || "",
createdAt: v.createdAt || Date.now()
});
videosAdded++;
}
}
}
writeArchive(archive, store);
return {
success: true,
sectorsAdded,
videosAdded
};
} catch {
return {
success: false,
sectorsAdded: 0,
videosAdded: 0
};
}
}
var RIBBON_SVG = `<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><path d="M4 2h8v12l-4-3-4 3z"/></svg>`;
function escapeHtml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
function showToast(message) {
let toast = document.querySelector(".br34-toast");
if (!toast) {
toast = document.createElement("div");
toast.className = "br34-toast";
document.body.append(toast);
}
toast.textContent = message;
toast.classList.add("visible");
window.clearTimeout(toast._timer);
toast._timer = window.setTimeout(() => {
toast?.classList.remove("visible");
}, 2200);
}
var ArchiveModal = class {
modal;
activeTab = "sectors";
isOpen = false;
videoSearchQuery = "";
getPage;
getUrl;
getListKey;
onDataChanged;
constructor(opts) {
this.getPage = opts.getPage;
this.getUrl = opts.getUrl;
this.getListKey = opts.getListKey;
this.onDataChanged = opts.onDataChanged;
this.modal = this.buildModal();
document.body.append(this.modal);
this.bindEvents();
}
toggle(open) {
this.isOpen = open !== void 0 ? open : !this.isOpen;
this.modal.classList.toggle("open", this.isOpen);
if (this.isOpen) this.render();
}
getIsOpen() {
return this.isOpen;
}
buildModal() {
const el = document.createElement("div");
el.className = "br34-archive-modal";
el.innerHTML = `
<div class="br34-panel-header">
<div class="br34-title-row">
<span class="br34-title">[ EROS // ARCHIVE ]</span>
<span class="br34-title-sub" id="br34-archive-counter">INDEX: 0 UNITS</span>
</div>
<button type="button" class="br34-panel-close" id="br34-archive-close" title="Close" aria-label="Close">[ X ]</button>
</div>
<div class="br34-archive-tabs">
<button type="button" class="br34-archive-tab active" data-tab="sectors">SECTORS (0)</button>
<button type="button" class="br34-archive-tab" data-tab="videos">SAVED VIDEOS (0)</button>
</div>
<div class="br34-archive-body" id="br34-archive-body">
<!-- Rendered dynamically -->
</div>
<div class="br34-archive-footer">
<button type="button" class="br34-archive-action-btn" id="br34-archive-export">EXPORT JSON</button>
<button type="button" class="br34-archive-action-btn" id="br34-archive-import">IMPORT JSON</button>
<button type="button" class="br34-archive-action-btn danger" id="br34-archive-clear">CLEAR TAB</button>
<input type="file" id="br34-archive-file-input" accept=".json,application/json" style="display:none;" />
</div>
`;
return el;
}
bindEvents() {
this.modal.querySelector("#br34-archive-close")?.addEventListener("click", () => {
this.toggle(false);
});
const tabBtns = this.modal.querySelectorAll(".br34-archive-tab");
for (const btn of tabBtns) btn.addEventListener("click", () => {
const tab = btn.dataset.tab;
if (tab && tab !== this.activeTab) {
this.activeTab = tab;
for (const b of tabBtns) b.classList.toggle("active", b.dataset.tab === tab);
this.render();
}
});
this.modal.querySelector("#br34-archive-export")?.addEventListener("click", () => {
const json = exportArchiveJson();
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `eros-archive-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
showToast("[ ARCHIVE EXPORTED ]");
});
const fileInput = this.modal.querySelector("#br34-archive-file-input");
this.modal.querySelector("#br34-archive-import")?.addEventListener("click", () => {
fileInput?.click();
});
fileInput?.addEventListener("change", () => {
const file = fileInput.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const res = importArchiveJson(String(e.target?.result || ""));
if (res.success) {
showToast(`[ IMPORTED: ${res.sectorsAdded} SECTORS, ${res.videosAdded} VIDEOS ]`);
this.render();
this.onDataChanged?.();
} else showToast("[ ERROR: INVALID ARCHIVE JSON ]");
};
reader.readAsText(file);
fileInput.value = "";
});
this.modal.querySelector("#br34-archive-clear")?.addEventListener("click", () => {
if (this.activeTab === "sectors") {
clearAllSectors();
showToast("[ ALL SECTORS CLEARED ]");
} else {
clearAllVideos();
showToast("[ ALL SAVED VIDEOS CLEARED ]");
}
this.render();
this.onDataChanged?.();
});
}
render() {
const sectors = getSectors();
const videos = getVideos();
const total = sectors.length + videos.length;
const counter = this.modal.querySelector("#br34-archive-counter");
if (counter) counter.textContent = `INDEX: ${total} UNITS`;
const sectorsTab = this.modal.querySelector("[data-tab=\"sectors\"]");
if (sectorsTab) sectorsTab.textContent = `SECTORS (${sectors.length})`;
const videosTab = this.modal.querySelector("[data-tab=\"videos\"]");
if (videosTab) videosTab.textContent = `SAVED VIDEOS (${videos.length})`;
const body = this.modal.querySelector("#br34-archive-body");
if (!body) return;
if (this.activeTab === "sectors") this.renderSectors(body, sectors);
else this.renderVideos(body, videos);
}
renderSectors(container, sectors) {
const curPage = this.getPage();
const curUrl = this.getUrl();
const listKey = this.getListKey();
const isCurrentSaved = Boolean(findSector(listKey, curPage));
let html = `
<div class="br34-archive-toolbar">
<button type="button" class="br34-btn-mark-sector ${isCurrentSaved ? "saved" : ""}" id="br34-mark-sector-btn">
${isCurrentSaved ? `[ ✓ SECTOR P.${curPage} SAVED (CLICK TO REMOVE) ]` : `[ + BOOKMARK CURRENT SECTOR // P.${curPage} ]`}
</button>
</div>
`;
if (sectors.length === 0) html += `
<div class="br34-archive-empty">
<div class="br34-archive-empty-title">[ NO SECTORS ARCHIVED ]</div>
<div class="br34-archive-empty-sub">Browse any catalog, tag, or search and click '+ Bookmark Current Sector'.</div>
</div>
`;
else {
html += `<div class="br34-archive-list">`;
for (const s of sectors) {
const isThisCurrent = s.listKey === listKey && s.page === curPage;
html += `
<div class="br34-archive-item ${isThisCurrent ? "is-current" : ""}">
<div class="br34-archive-item-main">
<div class="br34-archive-item-title">${escapeHtml(s.title || "SECTOR")}</div>
<div class="br34-archive-item-meta">
<span class="br34-tag-page">PAGE ${s.page}</span>
<span class="br34-tag-date">${new Date(s.createdAt).toLocaleDateString()}</span>
</div>
</div>
<div class="br34-archive-item-actions">
<a href="${escapeHtml(s.url)}" class="br34-btn-jump" title="Jump to page depth">[ JUMP ]</a>
<button type="button" class="br34-btn-del" data-del-sector="${escapeHtml(s.id)}" title="Delete sector" aria-label="Delete sector">[ ✕ ]</button>
</div>
</div>
`;
}
html += `</div>`;
}
container.innerHTML = html;
container.querySelector("#br34-mark-sector-btn")?.addEventListener("click", () => {
const existing = findSector(listKey, curPage);
if (existing) {
deleteSector(existing.id);
showToast(`[ REMOVED SECTOR // P.${curPage} ]`);
} else {
saveSector({
listKey,
title: formatSectorTitleFromUrl(curUrl),
url: curUrl,
page: curPage
});
showToast(`[ SAVED SECTOR // P.${curPage} ]`);
}
this.render();
this.onDataChanged?.();
});
const delBtns = container.querySelectorAll("[data-del-sector]");
for (const btn of delBtns) btn.addEventListener("click", () => {
const id = btn.dataset.delSector;
if (id) {
deleteSector(id);
this.render();
this.onDataChanged?.();
}
});
}
renderVideos(container, videos) {
const filtered = this.videoSearchQuery.trim() ? videos.filter((v) => v.title.toLowerCase().includes(this.videoSearchQuery.toLowerCase())) : videos;
let html = `
<div class="br34-archive-toolbar">
<input type="text" class="br34-search-input" id="br34-archive-video-search" placeholder="SEARCH SAVED VIDEOS..." value="${escapeHtml(this.videoSearchQuery)}" />
</div>
`;
if (videos.length === 0) html += `
<div class="br34-archive-empty">
<div class="br34-archive-empty-title">[ ARCHIVE EMPTY ]</div>
<div class="br34-archive-empty-sub">Hover any video thumbnail and click the ribbon icon to save videos for later.</div>
</div>
`;
else if (filtered.length === 0) html += `
<div class="br34-archive-empty">
<div class="br34-archive-empty-title">[ NO MATCHING VIDEOS ]</div>
</div>
`;
else {
html += `<div class="br34-video-archive-list">`;
for (const v of filtered) html += `
<div class="br34-video-archive-card">
${v.thumbUrl ? `<div class="br34-video-archive-thumb-wrap">
<img class="br34-video-archive-thumb" src="${escapeHtml(v.thumbUrl)}" alt="" loading="lazy" />
${v.durationFormatted ? `<span class="br34-video-archive-dur">${escapeHtml(v.durationFormatted)}</span>` : ""}
</div>` : ""}
<div class="br34-video-archive-info">
<a href="${escapeHtml(v.url)}" target="_blank" rel="noopener" class="br34-video-archive-title">${escapeHtml(v.title || "Untitled Video")}</a>
<div class="br34-video-archive-meta">
${v.ratingPercent > 0 ? `<span class="br34-video-archive-rating">${v.ratingPercent}%</span>` : ""}
${v.viewsFormatted ? `<span class="br34-video-archive-views">${escapeHtml(v.viewsFormatted)}</span>` : ""}
</div>
</div>
<div class="br34-video-archive-actions">
<a href="${escapeHtml(v.url)}" target="_blank" rel="noopener" class="br34-btn-jump">[ WATCH ]</a>
<button type="button" class="br34-btn-del" data-del-video="${escapeHtml(v.id)}" title="Remove" aria-label="Remove">[ ✕ ]</button>
</div>
</div>
`;
html += `</div>`;
}
container.innerHTML = html;
const searchInput = container.querySelector("#br34-archive-video-search");
searchInput?.addEventListener("input", () => {
this.videoSearchQuery = searchInput.value;
this.render();
});
const delBtns = container.querySelectorAll("[data-del-video]");
for (const btn of delBtns) btn.addEventListener("click", () => {
const id = btn.dataset.delVideo;
if (id) {
deleteVideo(id);
this.render();
this.onDataChanged?.();
}
});
}
destroy() {
this.modal.remove();
}
};
function attachCardBookmarkButtons(cards, onUpdate) {
for (const card of cards) {
if (card.dataset.br34BmWired === "true") continue;
card.dataset.br34BmWired = "true";
const wrap = card.querySelector(".wrap_image, .img.wrap_image");
if (!wrap) continue;
const data = extractCardData(card);
if (!data || !data.id) continue;
const ribbon = document.createElement("button");
ribbon.type = "button";
ribbon.className = `br34-card-bookmark ${isVideoSaved(data.id) ? "saved" : ""}`;
ribbon.title = isVideoSaved(data.id) ? "Saved in Archive (Click to remove)" : "Save to Archive";
ribbon.setAttribute("aria-label", ribbon.title);
ribbon.innerHTML = RIBBON_SVG;
ribbon.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (isVideoSaved(data.id)) {
deleteVideo(data.id);
ribbon.classList.remove("saved");
ribbon.title = "Save to Archive";
ribbon.setAttribute("aria-label", ribbon.title);
showToast("[ REMOVED FROM ARCHIVE ]");
} else {
saveVideo({
id: data.id,
title: data.title,
url: data.url,
thumbUrl: data.thumbUrl,
durationFormatted: data.durationFormatted,
ratingPercent: data.ratingPercent,
viewsFormatted: data.viewsFormatted
});
ribbon.classList.add("saved");
ribbon.title = "Saved in Archive (Click to remove)";
ribbon.setAttribute("aria-label", ribbon.title);
showToast("[ SAVED TO ARCHIVE ]");
}
onUpdate?.();
});
wrap.append(ribbon);
}
}
function mountBookmarkButton(opts) {
let dock = document.querySelector(".br34-dock");
if (!dock) {
dock = document.createElement("div");
dock.className = "br34-dock";
document.body.append(dock);
}
dock.append(opts.fab);
let btn = dock.querySelector(".br34-bookmark-btn");
if (!btn) {
btn = document.createElement("button");
btn.type = "button";
btn.className = "br34-bookmark-btn";
btn.innerHTML = `${RIBBON_SVG}<span class="br34-bookmark-badge">0</span>`;
dock.prepend(btn);
}
const button = btn;
const modal = new ArchiveModal({
getPage: opts.getPage,
getUrl: opts.getUrl,
getListKey: () => opts.listKey,
onDataChanged: () => {
updateBadge();
}
});
const updateBadge = () => {
const count = getTotalBookmarkCount();
const isCurrentSectorSaved = Boolean(findSector(opts.listKey, opts.getPage()));
button.classList.toggle("saved", isCurrentSectorSaved || count > 0);
let badge = button.querySelector(".br34-bookmark-badge");
if (count > 0) {
if (!badge) {
badge = document.createElement("span");
badge.className = "br34-bookmark-badge";
button.append(badge);
}
badge.textContent = String(count);
} else badge?.remove();
button.title = `EROS Archive (${count} saved items) — Click to open`;
button.setAttribute("aria-label", button.title);
};
updateBadge();
if (button.dataset.br34Wired !== "true") {
button.dataset.br34Wired = "true";
button.addEventListener("click", (e) => {
e.stopPropagation();
modal.toggle();
});
}
const outsideClickListener = (e) => {
if (modal.getIsOpen() && !modal["modal"].contains(e.target) && !button.contains(e.target)) modal.toggle(false);
};
document.addEventListener("click", outsideClickListener);
return {
cleanup: () => {
document.removeEventListener("click", outsideClickListener);
modal.destroy();
button.remove();
},
refresh: () => {
updateBadge();
if (modal.getIsOpen()) modal.render();
}
};
}
var STORAGE_KEY = "better_rule34_settings";
var VIEWS_STEPS = [
0,
1e3,
5e3,
1e4,
25e3,
5e4,
1e5
];
var DEBOUNCE_MS = 80;
var FilterBar = class {
state;
callbacks;
fabElement;
panelElement;
isOpen = false;
outsideClickHandler = null;
sliderDebounce = 0;
constructor(callbacks) {
this.callbacks = callbacks;
this.state = this.loadInitialState();
this.fabElement = this.buildFab();
this.panelElement = this.buildPanel();
const searchInput = this.panelElement.querySelector(".br34-search-input");
if (searchInput) searchInput.value = this.state.query;
this.mount();
}
loadInitialState() {
const urlFilters = parseCurrentUrlFilters(window.location.href);
let savedSettings = {};
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) savedSettings = JSON.parse(raw);
} catch {}
return {
...DEFAULT_FILTER,
...savedSettings,
...urlFilters
};
}
saveSettings() {
try {
const toSave = {
soundOnly: this.state.soundOnly,
hdOnly: this.state.hdOnly,
futaFilter: this.state.futaFilter,
hideWatched: this.state.hideWatched,
minRating: this.state.minRating,
minViews: this.state.minViews,
minYear: this.state.minYear,
durationMinSeconds: this.state.durationMinSeconds
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
} catch {}
}
getState() {
return this.state;
}
mount() {
document.body.append(this.fabElement, this.panelElement);
this.bindEvents();
this.updateBadge();
}
countActiveFilters() {
let count = 0;
if (this.state.query.trim()) count++;
if (this.state.soundOnly) count++;
if (this.state.hdOnly) count++;
if (this.state.futaFilter !== "all") count++;
if (this.state.hideWatched) count++;
if (this.state.minRating > 0) count++;
if (this.state.minViews > 0) count++;
if (this.state.minYear > 2018) count++;
if (this.state.durationMinSeconds !== null && this.state.durationMinSeconds > 0) count++;
return count;
}
updateBadge() {
const count = this.countActiveFilters();
let badge = this.fabElement.querySelector(".br34-fab-badge");
if (count > 0) {
if (!badge) {
badge = document.createElement("span");
badge.className = "br34-fab-badge";
this.fabElement.append(badge);
}
badge.textContent = `[${count}]`;
} else badge?.remove();
}
setCount(visible, total) {
const counter = this.panelElement.querySelector(".br34-title-sub");
if (counter) counter.textContent = `UNITS: ${visible} / ${total}`;
}
buildFab() {
const fab = document.createElement("button");
fab.type = "button";
fab.className = "br34-fab";
fab.title = "Open EROS Telemetry Filter";
fab.setAttribute("aria-label", "Open EROS Telemetry Filter");
fab.innerHTML = `
<span class="br34-fab-dot"></span>
<span>CTRL</span>
`;
return fab;
}
buildPanel() {
const panel = document.createElement("div");
panel.className = "br34-panel";
const currentYear = new Date().getFullYear();
const durMins = this.state.durationMinSeconds ? Math.round(this.state.durationMinSeconds / 60) : 0;
panel.innerHTML = `
<!-- Header: Title, Units & Close -->
<div class="br34-panel-header">
<div class="br34-title-row">
<span class="br34-title">[ EROS // TELEMETRY ]</span>
<span class="br34-title-sub">UNITS: LOADING...</span>
</div>
<button type="button" class="br34-panel-close" title="Close" aria-label="Close">[ X ]</button>
</div>
<!-- Body -->
<div class="br34-panel-body">
<!-- Live Keyword Search -->
<div class="br34-search-box">
<input type="text" class="br34-search-input" placeholder="SEARCH // TITLE KEYWORD" value="" />
</div>
<!-- Tactile Sliders (Vertical Stack) -->
<div class="br34-sliders-vertical">
<!-- 1. Rating Slider -->
<div class="br34-slider-card">
<div class="br34-sect-title">
<span>MIN RATING</span>
<span class="br34-sect-val" id="br34-val-rating">${this.state.minRating > 0 ? `≥ ${this.state.minRating}%` : "ANY"}</span>
</div>
<input type="range" class="br34-range-slider" id="br34-slider-rating" min="0" max="100" step="5" value="${this.state.minRating}" />
</div>
<!-- 2. Views Slider -->
<div class="br34-slider-card">
<div class="br34-sect-title">
<span>MIN VIEWS</span>
<span class="br34-sect-val" id="br34-val-views">${this.formatViewsLabel(this.state.minViews)}</span>
</div>
<input type="range" class="br34-range-slider" id="br34-slider-views" min="0" max="6" step="1" value="${viewsToNearestStep(this.state.minViews, VIEWS_STEPS)}" />
</div>
<!-- 3. Duration Slider -->
<div class="br34-slider-card">
<div class="br34-sect-title">
<span>MIN DURATION</span>
<span class="br34-sect-val" id="br34-val-dur">${durMins > 0 ? `≥ ${durMins}M` : "ANY"}</span>
</div>
<input type="range" class="br34-range-slider" id="br34-slider-dur" min="0" max="45" step="1" value="${durMins}" />
</div>
<!-- 4. Vintage Slider -->
<div class="br34-slider-card">
<div class="br34-sect-title">
<span>MIN VINTAGE</span>
<span class="br34-sect-val" id="br34-val-year">${this.state.minYear > 2018 ? `≥ ${this.state.minYear}` : "ALL"}</span>
</div>
<input type="range" class="br34-range-slider" id="br34-slider-year" min="${BASE_YEAR}" max="${currentYear}" step="1" value="${this.state.minYear}" />
</div>
</div>
<!-- Tactile Toggles (Compact 2x2 grid) -->
<div class="br34-grid-2x2">
<button type="button" class="br34-chip ${this.state.soundOnly ? "active" : ""}" data-toggle="sound">
SOUND
</button>
<button type="button" class="br34-chip ${this.state.hdOnly ? "active" : ""}" data-toggle="hd">
HD ONLY
</button>
<button type="button" class="br34-chip ${this.state.futaFilter !== "all" ? "active-purple" : ""}" data-toggle="futa">
${this.getFutaLabel()}
</button>
<button type="button" class="br34-chip ${this.state.hideWatched ? "active" : ""}" data-toggle="watched">
UNWATCHED
</button>
</div>
</div>
<!-- Footer: Reset -->
<div class="br34-panel-footer">
<button type="button" class="br34-btn-reset" id="br34-btn-reset">
RESET ALL FILTERS
</button>
</div>
`;
return panel;
}
formatViewsLabel(views) {
if (views <= 0) return "ANY";
if (views >= 1e6) return `≥ ${views / 1e6}M`;
if (views >= 1e3) return `≥ ${views / 1e3}K`;
return `≥ ${views}`;
}
getFutaLabel() {
switch (this.state.futaFilter) {
case "hide": return "NO FUTA";
case "only": return "FUTA ONLY";
default: return "FUTA: ALL";
}
}
cycleFuta() {
if (this.state.futaFilter === "all") this.state.futaFilter = "hide";
else if (this.state.futaFilter === "hide") this.state.futaFilter = "only";
else this.state.futaFilter = "all";
const btn = this.panelElement.querySelector("[data-toggle=\"futa\"]");
if (btn) {
btn.textContent = this.getFutaLabel();
btn.classList.toggle("active-purple", this.state.futaFilter !== "all");
}
this.saveSettings();
this.updateBadge();
this.callbacks.onFilterChange(this.state);
}
togglePanel(open) {
this.isOpen = open !== void 0 ? open : !this.isOpen;
this.panelElement.classList.toggle("open", this.isOpen);
this.fabElement.classList.toggle("active", this.isOpen);
}
commitSliderChange() {
window.clearTimeout(this.sliderDebounce);
this.sliderDebounce = window.setTimeout(() => {
this.saveSettings();
this.updateBadge();
this.callbacks.onFilterChange(this.state);
}, DEBOUNCE_MS);
}
bindToggle(selector, isActive, onToggle) {
const btn = this.panelElement.querySelector(selector);
btn?.addEventListener("click", () => {
onToggle();
btn.classList.toggle("active", isActive());
this.saveSettings();
this.updateBadge();
this.callbacks.onFilterChange(this.state);
});
}
bindEvents() {
this.fabElement.addEventListener("click", (e) => {
e.stopPropagation();
this.togglePanel();
});
this.panelElement.querySelector(".br34-panel-close")?.addEventListener("click", () => {
this.togglePanel(false);
});
this.outsideClickHandler = (e) => {
if (this.isOpen && !this.panelElement.contains(e.target) && !this.fabElement.contains(e.target)) this.togglePanel(false);
};
document.addEventListener("click", this.outsideClickHandler);
let searchDebounce = 0;
const searchInput = this.panelElement.querySelector(".br34-search-input");
searchInput?.addEventListener("input", () => {
window.clearTimeout(searchDebounce);
searchDebounce = window.setTimeout(() => {
this.state.query = searchInput.value;
this.updateBadge();
this.callbacks.onFilterChange(this.state);
}, DEBOUNCE_MS);
});
const ratingSlider = this.panelElement.querySelector("#br34-slider-rating");
const ratingVal = this.panelElement.querySelector("#br34-val-rating");
ratingSlider?.addEventListener("input", () => {
const val = parseInt(ratingSlider.value, 10);
this.state.minRating = val;
if (ratingVal) ratingVal.textContent = val > 0 ? `≥ ${val}%` : "ANY";
this.commitSliderChange();
});
const viewsSlider = this.panelElement.querySelector("#br34-slider-views");
const viewsVal = this.panelElement.querySelector("#br34-val-views");
viewsSlider?.addEventListener("input", () => {
const val = VIEWS_STEPS[parseInt(viewsSlider.value, 10)] || 0;
this.state.minViews = val;
if (viewsVal) viewsVal.textContent = this.formatViewsLabel(val);
this.commitSliderChange();
});
const durSlider = this.panelElement.querySelector("#br34-slider-dur");
const durVal = this.panelElement.querySelector("#br34-val-dur");
durSlider?.addEventListener("input", () => {
const mins = parseInt(durSlider.value, 10);
this.state.durationMinSeconds = mins > 0 ? mins * 60 : null;
if (durVal) durVal.textContent = mins > 0 ? `≥ ${mins}M` : "ANY";
this.commitSliderChange();
});
const yearSlider = this.panelElement.querySelector("#br34-slider-year");
const yearVal = this.panelElement.querySelector("#br34-val-year");
yearSlider?.addEventListener("input", () => {
const yr = parseInt(yearSlider.value, 10);
this.state.minYear = yr;
if (yearVal) yearVal.textContent = yr > 2018 ? `≥ ${yr}` : "ALL";
this.commitSliderChange();
});
this.bindToggle("[data-toggle=\"sound\"]", () => this.state.soundOnly, () => {
this.state.soundOnly = !this.state.soundOnly;
});
this.bindToggle("[data-toggle=\"hd\"]", () => this.state.hdOnly, () => {
this.state.hdOnly = !this.state.hdOnly;
});
this.panelElement.querySelector("[data-toggle=\"futa\"]")?.addEventListener("click", () => {
this.cycleFuta();
});
this.bindToggle("[data-toggle=\"watched\"]", () => this.state.hideWatched, () => {
this.state.hideWatched = !this.state.hideWatched;
});
this.panelElement.querySelector("#br34-btn-reset")?.addEventListener("click", () => {
this.state = { ...DEFAULT_FILTER };
this.saveSettings();
this.syncInputsWithState();
this.updateBadge();
this.callbacks.onFilterChange(this.state);
});
}
syncInputsWithState() {
const searchInput = this.panelElement.querySelector(".br34-search-input");
if (searchInput) searchInput.value = this.state.query;
const ratingSlider = this.panelElement.querySelector("#br34-slider-rating");
const ratingVal = this.panelElement.querySelector("#br34-val-rating");
if (ratingSlider) ratingSlider.value = String(this.state.minRating);
if (ratingVal) ratingVal.textContent = this.state.minRating > 0 ? `≥ ${this.state.minRating}%` : "ANY";
const viewsSlider = this.panelElement.querySelector("#br34-slider-views");
const viewsVal = this.panelElement.querySelector("#br34-val-views");
if (viewsSlider) viewsSlider.value = String(viewsToNearestStep(this.state.minViews, VIEWS_STEPS));
if (viewsVal) viewsVal.textContent = this.formatViewsLabel(this.state.minViews);
const durSlider = this.panelElement.querySelector("#br34-slider-dur");
const durVal = this.panelElement.querySelector("#br34-val-dur");
const durMins = this.state.durationMinSeconds ? Math.round(this.state.durationMinSeconds / 60) : 0;
if (durSlider) durSlider.value = String(durMins);
if (durVal) durVal.textContent = durMins > 0 ? `≥ ${durMins}M` : "ANY";
const yearSlider = this.panelElement.querySelector("#br34-slider-year");
const yearVal = this.panelElement.querySelector("#br34-val-year");
if (yearSlider) yearSlider.value = String(this.state.minYear);
if (yearVal) yearVal.textContent = this.state.minYear > 2018 ? `≥ ${this.state.minYear}` : "ALL";
this.panelElement.querySelector("[data-toggle=\"sound\"]")?.classList.toggle("active", this.state.soundOnly);
this.panelElement.querySelector("[data-toggle=\"hd\"]")?.classList.toggle("active", this.state.hdOnly);
const futaBtn = this.panelElement.querySelector("[data-toggle=\"futa\"]");
if (futaBtn) {
futaBtn.textContent = this.getFutaLabel();
futaBtn.classList.toggle("active-purple", this.state.futaFilter !== "all");
}
this.panelElement.querySelector("[data-toggle=\"watched\"]")?.classList.toggle("active", this.state.hideWatched);
}
destroy() {
if (this.outsideClickHandler) {
document.removeEventListener("click", this.outsideClickHandler);
this.outsideClickHandler = null;
}
window.clearTimeout(this.sliderDebounce);
this.fabElement.remove();
this.panelElement.remove();
}
};
var COLLAPSE_KEY = "better_rule34_native_filters_collapsed_v1";
var TOGGLE_LABEL = "<span>[ SITE FILTERS ]</span>";
var TOGGLE_ICON = "<svg class=\"filters-panel__toggle-icon\" viewBox=\"0 0 12 12\" width=\"12\" height=\"12\" aria-hidden=\"true\"><path d=\"M2 4l4 4 4-4\"/></svg>";
function resolveStore(store) {
if (store) return store;
try {
if (typeof localStorage !== "undefined") return localStorage;
} catch {}
return null;
}
function shouldCollapseNativeFilters(store) {
const s = resolveStore(store);
if (!s) return true;
try {
const raw = s.getItem(COLLAPSE_KEY);
if (raw === null) return true;
return raw !== "0";
} catch {
return true;
}
}
function setNativeFiltersCollapsed(collapsed, store) {
const s = resolveStore(store);
if (!s) return;
try {
s.setItem(COLLAPSE_KEY, collapsed ? "1" : "0");
} catch {}
}
function setCollapsed(panel, toggle, collapsed) {
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
panel.classList.toggle("br34-collapsed", collapsed);
setNativeFiltersCollapsed(collapsed);
}
function wirePanel(panel, collapsed) {
let toggle = panel.querySelector(".filters-panel__toggle");
let body = panel.querySelector(".filters-panel__body");
if (!body) {
body = document.createElement("div");
body.className = "filters-panel__body";
for (const child of Array.from(panel.childNodes)) if (child !== toggle) body.append(child);
panel.append(body);
}
if (!toggle) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "filters-panel__toggle";
btn.innerHTML = `${TOGGLE_LABEL}${TOGGLE_ICON}`;
panel.prepend(btn);
toggle = btn;
} else if (!toggle.querySelector(".filters-panel__toggle-icon")) {
const icon = document.createElement("span");
icon.className = "filters-panel__toggle-icon";
icon.setAttribute("aria-hidden", "true");
toggle.append(icon);
}
if (!toggle.hasAttribute("aria-expanded")) toggle.setAttribute("aria-expanded", "true");
if (toggle instanceof HTMLButtonElement && !toggle.hasAttribute("type")) toggle.setAttribute("type", "button");
if (toggle.dataset.br34FilterToggleWired !== "true") {
toggle.dataset.br34FilterToggleWired = "true";
const btn = toggle;
btn.addEventListener("click", (e) => {
e.preventDefault();
const isExpanded = btn.getAttribute("aria-expanded") !== "false";
setCollapsed(panel, btn, isExpanded);
});
}
setCollapsed(panel, toggle, collapsed);
}
function initNativeFilterPanel() {
const collapsed = shouldCollapseNativeFilters();
const panels = document.querySelectorAll(".filters-panel");
for (const panel of panels) try {
wirePanel(panel, collapsed);
} catch {}
}
var CSS = `
/* ==========================================================================
Better Rule34Video - Industrial Brutalism + Erotic Latex Edition
========================================================================== */
/* --- SITE-WIDE THEME ENHANCEMENTS --- */
body {
background: #08060a !important;
color: #e5e5e9 !important;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
}
/* Base substrate texture */
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 99999;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 3px,
rgba(255, 0, 85, 0.015) 3px,
rgba(255, 0, 85, 0.015) 6px
);
opacity: 0.6;
}
/* Header & Container Cleanup */
.header {
background: #0a070e !important;
border-bottom: 1px solid #ff0055 !important;
box-shadow: 0 4px 20px rgba(255, 0, 85, 0.15) !important;
}
.logo a svg {
filter: drop-shadow(0 0 8px rgba(255, 0, 85, 0.6));
}
.headline .title {
font-family: 'JetBrains Mono', 'IBM Plex Mono', monospace !important;
text-transform: uppercase !important;
letter-spacing: 0.06em !important;
color: #ffffff !important;
border-left: 3px solid #ff0055;
padding-left: 10px;
}
/* Card Enhancements - Brutalist Erotic Precision Frame */
.item.thumb {
background: #0e0a14 !important;
border: 1px solid rgba(255, 0, 85, 0.25) !important;
border-radius: 4px !important;
padding: 6px 6px 8px 6px !important;
margin-bottom: 16px !important;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.7) !important;
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
.item.thumb:hover {
transform: translateY(-3px) !important;
border-color: #ff0055 !important;
box-shadow: 0 8px 24px rgba(255, 0, 85, 0.35), 0 0 10px rgba(255, 0, 85, 0.15) !important;
}
.item.thumb a.th,
.item.thumb .th {
display: block !important;
text-decoration: none !important;
color: inherit !important;
width: 100% !important;
outline: none !important;
}
.item.thumb .img.wrap_image,
.item.thumb .wrap_image,
.th .wrap_image {
position: relative !important;
border-radius: 2px !important;
overflow: hidden !important;
background: #050307 !important;
border: 1px solid rgba(255, 255, 255, 0.06) !important;
width: 100% !important;
display: block !important;
}
.item.thumb .wrap_image img {
width: 100% !important;
height: auto !important;
display: block !important;
}
.item.thumb .thumb_title {
color: #f1f1f5 !important;
font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
font-size: 12.5px !important;
font-weight: 600 !important;
line-height: 1.35 !important;
height: 2.7em !important;
max-height: 2.7em !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 2 !important;
margin: 6px 0 4px 0 !important;
padding: 0 2px !important;
transition: color 0.15s ease !important;
}
.item.thumb:hover .thumb_title {
color: #ff0055 !important;
}
/* Metadata Row (Clean 2-item layout: Rating on left, Views on right - NO overlap) */
.item.thumb .thumb_info,
.item.thumb .video-card-meta {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 6px !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 11px !important;
color: #8c8998 !important;
padding: 2px 2px 0 2px !important;
margin: 0 !important;
line-height: 1.2 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* Rating */
.item.thumb .thumb_info .rating,
.item.thumb .video-card-meta__rating {
color: #00e676 !important;
font-weight: 700 !important;
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
white-space: nowrap !important;
}
.item.thumb .thumb_info .rating svg,
.item.thumb .video-card-meta__rating svg {
fill: #00e676 !important;
width: 12px !important;
height: 12px !important;
}
/* Views (Explicitly targeting video-views-count so comment count is never matched) */
.item.thumb .video-views-count,
.item.thumb .video-card-meta__views {
color: #a5a2b3 !important;
font-weight: 700 !important;
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
white-space: nowrap !important;
}
.item.thumb .video-views-count svg,
.item.thumb .video-card-meta__views svg {
fill: #a5a2b3 !important;
width: 12px !important;
height: 12px !important;
}
/* HIDE "x min / hours ago" as requested */
.item.thumb .thumb_info .added,
.item.thumb .video-card-meta__date,
.item.thumb .added,
.thumb_info .added,
.video-card-meta__date {
display: none !important;
visibility: hidden !important;
width: 0 !important;
height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
/* HIDE comments on cards only (scoped: watch-page #comments_box untouched) */
.item.thumb .video-comments-count,
.item.thumb .video-card-meta__comments,
.item.thumb .comments,
.item.thumb .comments-count,
.item.thumb [class*="comment" i],
.item.thumb [title*="comment" i],
.item.thumb [aria-label*="comment" i],
.item.thumb .thumb_info .comments {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
width: 0 !important;
height: 0 !important;
pointer-events: none !important;
}
/* Suppress ads */
.spot-thumb,
.spots,
.sidebar_ad_buttons,
.footer_spots,
ins.adsbyjuicy,
.item.thumb:has(header),
.item.thumb:has(iframe) {
display: none !important;
}
.item.thumb[data-br34-hidden="true"] {
display: none !important;
}
/* ==========================================================================
Rethemed Native Filters & Sorting Panel (Industrial Brutalist)
========================================================================== */
.filters-panel {
display: block !important;
background: #0e0a14 !important;
border: 1px solid rgba(255, 0, 85, 0.35) !important;
border-radius: 6px !important;
margin: 14px 0 20px 0 !important;
padding: 0 !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.7), 0 0 15px rgba(255, 0, 85, 0.1) !important;
overflow: hidden !important;
font-family: 'JetBrains Mono', monospace !important;
}
.filters-panel__toggle {
width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 10px 16px !important;
background: rgba(16, 10, 22, 0.95) !important;
border: none !important;
border-bottom: 1px solid rgba(255, 0, 85, 0.25) !important;
color: #ff0055 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 11.5px !important;
font-weight: 800 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
cursor: pointer !important;
outline: none !important;
transition: background 0.15s ease, color 0.15s ease !important;
}
.filters-panel__toggle:hover {
background: rgba(255, 0, 85, 0.12) !important;
color: #ffffff !important;
}
.filters-panel__toggle-icon {
fill: #ff0055 !important;
width: 12px !important;
height: 12px !important;
transition: transform 0.2s ease !important;
}
.filters-panel__toggle[aria-expanded="false"] .filters-panel__toggle-icon {
transform: rotate(-90deg) !important;
}
/* Collapsed state (wired by nativefilter.ts): hide the body, flatten toggle */
.filters-panel.br34-collapsed .filters-panel__body {
display: none !important;
}
.filters-panel__toggle[aria-expanded="false"] {
border-bottom: none !important;
}
.filters-panel__body {
padding: 14px 16px !important;
display: flex !important;
flex-direction: column !important;
gap: 12px !important;
background: #0a070e !important;
}
.filters-panel__section {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
border-bottom: 1px dashed rgba(255, 0, 85, 0.15) !important;
padding-bottom: 10px !important;
}
.filters-panel__section:last-child {
border-bottom: none !important;
padding-bottom: 0 !important;
}
.filters-panel__label,
.filters-group__label {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
font-size: 10px !important;
font-weight: 800 !important;
letter-spacing: 0.06em !important;
text-transform: uppercase !important;
color: #9893a6 !important;
margin-bottom: 2px !important;
}
.filters-panel__label svg,
.filters-group__label svg {
fill: #ff0055 !important;
width: 12px !important;
height: 12px !important;
}
.filters-panel .btn,
.filters-panel__controls--chips .btn,
.filters-group__controls .btn {
background: #0f0b17 !important;
border: 1px solid rgba(255, 255, 255, 0.15) !important;
border-radius: 4px !important;
color: #a39eb0 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 10px !important;
font-weight: 700 !important;
letter-spacing: 0.04em !important;
text-transform: uppercase !important;
padding: 5px 10px !important;
text-decoration: none !important;
cursor: pointer !important;
display: inline-flex !important;
align-items: center !important;
transition: all 0.14s ease !important;
line-height: 1.3 !important;
}
.filters-panel .btn:hover,
.filters-panel__controls--chips .btn:hover,
.filters-group__controls .btn:hover {
border-color: #ff0055 !important;
color: #ffffff !important;
box-shadow: 0 0 8px rgba(255, 0, 85, 0.3) !important;
}
.filters-panel .btn.active,
.filters-panel__controls--chips .btn.active,
.filters-group__controls .btn.active {
background: #ff0055 !important;
border-color: #ff0055 !important;
color: #000000 !important;
font-weight: 900 !important;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.55) !important;
}
.filters-panel .btn_custom {
position: relative !important;
background: #0f0b17 !important;
border: 1px solid rgba(255, 0, 85, 0.35) !important;
color: #ffffff !important;
}
.filters-panel .date-filter-dropdown,
.filters-panel .filter-custom {
background: #0e0a14 !important;
border: 1px solid #ff0055 !important;
border-radius: 4px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.9), 0 0 15px rgba(255, 0, 85, 0.25) !important;
padding: 4px 0 !important;
z-index: 1000 !important;
}
.filters-panel .date-filter-dropdown li a,
.filters-panel .filter-custom li a {
color: #c5c2d3 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 10px !important;
padding: 5px 12px !important;
display: block !important;
text-decoration: none !important;
transition: background 0.12s ease, color 0.12s ease !important;
}
.filters-panel .date-filter-dropdown li a:hover,
.filters-panel .filter-custom li a:hover {
background: rgba(255, 0, 85, 0.15) !important;
color: #ff0055 !important;
}
.filters-panel .date-filter-dropdown li a.active,
.filters-panel .filter-custom li a.active {
background: #ff0055 !important;
color: #000000 !important;
font-weight: 800 !important;
}
.filters-panel input[type="date"],
.filters-panel input[type="number"],
.filters-panel .duration-filter__input {
background: #050307 !important;
border: 1px solid rgba(255, 0, 85, 0.35) !important;
border-radius: 4px !important;
color: #ffffff !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 10px !important;
padding: 4px 8px !important;
outline: none !important;
transition: border-color 0.15s ease, box-shadow 0.15s ease !important;
}
.filters-panel input[type="date"]:focus,
.filters-panel input[type="number"]:focus,
.filters-panel .duration-filter__input:focus {
border-color: #ff0055 !important;
box-shadow: 0 0 8px rgba(255, 0, 85, 0.4) !important;
}
.filters-panel .duration-filter__label {
color: #9893a6 !important;
font-size: 9.5px !important;
font-weight: 700 !important;
margin-right: 4px !important;
}
.filters-panel .duration-filter__apply {
background: rgba(18, 12, 24, 0.9) !important;
border: 1px solid rgba(255, 0, 85, 0.4) !important;
border-radius: 4px !important;
color: #e5e5eb !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 10px !important;
font-weight: 800 !important;
padding: 4px 10px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
}
.filters-panel .duration-filter__apply:hover {
background: #ff0055 !important;
color: #000000 !important;
box-shadow: 0 0 8px rgba(255, 0, 85, 0.6) !important;
}
/* ==========================================================================
Floating Brutalist FAB Button
========================================================================== */
.br34-fab {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483647;
height: 40px;
padding: 0 14px;
border-radius: 8px;
background: rgba(9, 6, 13, 0.95);
border: 1px solid #ff0055;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.8), 0 0 14px rgba(255, 0, 85, 0.35);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #ffffff;
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
outline: none;
user-select: none;
transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.br34-fab:hover {
background: #ff0055;
color: #000000;
box-shadow: 0 0 25px rgba(255, 0, 85, 0.8);
transform: translateY(-2px);
}
.br34-fab:active {
transform: translateY(0);
}
.br34-fab-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff0055;
box-shadow: 0 0 8px #ff0055;
display: inline-block;
animation: br34-dot-blink 1.4s infinite ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.br34-fab-dot,
.br34-spinner {
animation: none;
}
}
.br34-fab:hover .br34-fab-dot {
background: #000000;
box-shadow: none;
}
@keyframes br34-dot-blink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.7); }
}
.br34-fab-badge {
background: #ff0055;
color: #000000;
font-size: 10px;
font-weight: 900;
padding: 1px 5px;
border-radius: 4px;
margin-left: 2px;
}
/* ==========================================================================
Floating Brutalist Telemetry Modal (Vertical Console)
========================================================================== */
.br34-panel {
position: fixed;
bottom: 74px;
right: 24px;
z-index: 2147483646;
width: 290px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 90px);
background: rgba(10, 7, 14, 0.97);
border: 1px solid rgba(255, 0, 85, 0.45);
border-radius: 8px;
box-shadow: 0 16px 50px rgba(0, 0, 0, 0.95), 0 0 25px rgba(255, 0, 85, 0.22);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
display: flex;
flex-direction: column;
color: #e5e5eb;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
opacity: 0;
transform: translateY(12px) scale(0.98);
pointer-events: none;
transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.br34-panel.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
/* Header */
.br34-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 0, 85, 0.25);
background: rgba(16, 10, 22, 0.95);
}
.br34-title-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.br34-title {
font-size: 11.5px;
font-weight: 900;
letter-spacing: 0.08em;
color: #ff0055;
text-transform: uppercase;
}
.br34-title-sub {
font-size: 9.5px;
font-weight: 700;
color: #9c97a8;
letter-spacing: 0.05em;
}
.br34-panel-close {
background: transparent;
border: 1px solid rgba(255, 0, 85, 0.4);
color: #ff0055;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
font-weight: 800;
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.br34-panel-close:hover {
background: #ff0055;
color: #000000;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.7);
}
/* Body */
.br34-panel-body {
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 11px;
overflow-y: auto;
max-height: calc(100vh - 165px);
}
/* Search input */
.br34-search-box {
width: 100%;
margin: 0;
}
.br34-search-input {
width: 100%;
height: 32px;
background: rgba(5, 3, 7, 0.95);
border: 1px solid rgba(255, 0, 85, 0.35);
border-radius: 5px;
color: #ffffff;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
padding: 6px 10px;
outline: none;
box-sizing: border-box;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.br34-search-input:focus {
border-color: #ff0055;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.4);
}
/* Sliders Stack (Vertical orientation) */
.br34-sliders-vertical {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
margin: 0;
padding: 0;
}
.br34-slider-card {
display: flex;
flex-direction: column;
}
.br34-sect-title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9893a6;
margin-bottom: 2px;
}
.br34-sect-val {
color: #ffffff;
font-weight: 800;
font-size: 9.5px;
background: rgba(255, 0, 85, 0.15);
border: 1px solid rgba(255, 0, 85, 0.6);
border-radius: 3px;
padding: 1px 6px;
}
.br34-range-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: #1c1322;
outline: none;
cursor: pointer;
margin: 3px 0;
accent-color: #ff0055;
}
.br34-range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #ff0055;
border: 1px solid #ffffff;
box-shadow: 0 0 8px #ff0055;
cursor: pointer;
transition: transform 0.1s ease;
}
.br34-range-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
background: #ffffff;
border-color: #ff0055;
}
.br34-range-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #ff0055;
border: 1px solid #ffffff;
box-shadow: 0 0 8px #ff0055;
cursor: pointer;
}
/* Grid 2x2 for Tactile Toggles */
.br34-grid-2x2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
width: 100%;
margin: 0;
}
/* Chips in grid */
.br34-chip {
background: #0f0b17;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 5px;
color: #a39eb0;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 7px 4px;
text-align: center;
cursor: pointer;
user-select: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.14s ease;
line-height: 1.2;
}
.br34-chip:hover {
border-color: #ff0055;
color: #ffffff;
}
.br34-chip.active {
background: #ff0055;
border-color: #ff0055;
color: #000000;
font-weight: 900;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.55);
}
.br34-chip.active-purple {
background: #bf00ff;
border-color: #bf00ff;
color: #000000;
font-weight: 900;
box-shadow: 0 0 10px rgba(191, 0, 255, 0.55);
}
/* Footer Actions */
.br34-panel-footer {
padding: 10px 14px;
border-top: 1px solid rgba(255, 0, 85, 0.25);
display: flex;
background: rgba(16, 10, 22, 0.95);
}
.br34-btn-reset {
width: 100%;
background: rgba(18, 12, 24, 0.9);
border: 1px solid rgba(255, 0, 85, 0.4);
border-radius: 5px;
color: #e5e5eb;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 8px 12px;
cursor: pointer;
transition: all 0.15s ease;
text-align: center;
}
.br34-btn-reset:hover {
background: #ff0055;
border-color: #ff0055;
color: #000000;
box-shadow: 0 0 14px rgba(255, 0, 85, 0.6);
}
/* ==========================================================================
Auto-Pager Telemetry Status
========================================================================== */
.br34-autopager-container {
width: 100%;
margin: 30px 0 50px;
text-align: center;
clear: both;
}
.br34-autopager-loading {
display: inline-flex;
align-items: center;
gap: 10px;
background: rgba(9, 6, 13, 0.95);
border: 1px solid #ff0055;
border-radius: 6px;
padding: 8px 18px;
color: #ffffff;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: 0 0 15px rgba(255, 0, 85, 0.35);
}
.br34-spinner {
width: 12px;
height: 12px;
border: 2px solid rgba(255, 0, 85, 0.2);
border-top-color: #ff0055;
border-radius: 50%;
animation: br34-spin 0.6s linear infinite;
}
@keyframes br34-spin {
to { transform: rotate(360deg); }
}
.br34-autopager-end {
display: inline-block;
background: rgba(9, 6, 13, 0.95);
border: 1px solid rgba(255, 0, 85, 0.3);
border-radius: 6px;
color: #8c8998;
font-family: 'JetBrains Mono', monospace;
padding: 8px 18px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.br34-load-more-btn {
background: #ff0055;
border: 1px solid #ff0055;
border-radius: 6px;
color: #000000;
font-family: 'JetBrains Mono', monospace;
padding: 8px 20px;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 0 15px rgba(255, 0, 85, 0.4);
transition: all 0.15s ease;
}
.br34-load-more-btn:hover {
background: #ffffff;
border-color: #ffffff;
color: #000000;
}
/* ==========================================================================
Page separator (auto-pager batch boundary: [ PAGE N ])
========================================================================== */
.br34-page-sep {
width: 100%;
flex-basis: 100%;
grid-column: 1 / -1;
clear: both;
text-align: center;
margin: 18px 0 22px;
padding: 7px 0;
border-top: 1px dashed rgba(255, 0, 85, 0.4);
border-bottom: 1px dashed rgba(255, 0, 85, 0.4);
color: #ff0055;
background: rgba(255, 0, 85, 0.05);
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.1em;
user-select: none;
}
/* ==========================================================================
Dock (bookmark button + CTRL fab, bottom-right)
========================================================================== */
.br34-dock {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483647;
display: flex;
align-items: center;
gap: 8px;
}
.br34-dock .br34-fab {
position: static;
}
.br34-bookmark-btn {
height: 40px;
width: 40px;
padding: 0;
border-radius: 8px;
background: rgba(9, 6, 13, 0.95);
border: 1px solid #ff0055;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.8), 0 0 14px rgba(255, 0, 85, 0.35);
color: #ff0055;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
user-select: none;
transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.br34-bookmark-btn:hover {
background: #ff0055;
color: #000000;
box-shadow: 0 0 25px rgba(255, 0, 85, 0.8);
transform: translateY(-2px);
}
.br34-bookmark-btn.saved {
background: #ff0055;
color: #000000;
box-shadow: 0 0 25px rgba(255, 0, 85, 0.8);
}
.br34-bookmark-btn.saved svg {
fill: currentColor;
}
/* Card Bookmark Ribbon */
.br34-card-bookmark {
position: absolute !important;
top: 6px !important;
right: 6px !important;
z-index: 15 !important;
width: 26px !important;
height: 26px !important;
padding: 0 !important;
background: rgba(10, 7, 14, 0.85) !important;
border: 1px solid rgba(255, 0, 85, 0.45) !important;
border-radius: 4px !important;
color: #ff0055 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
opacity: 0 !important;
transform: scale(0.9) !important;
transition: all 0.15s ease !important;
outline: none !important;
}
.item.thumb:hover .br34-card-bookmark {
opacity: 1 !important;
transform: scale(1) !important;
}
.br34-card-bookmark:hover {
background: #ff0055 !important;
color: #000000 !important;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.8) !important;
}
.br34-card-bookmark.saved {
opacity: 1 !important;
transform: scale(1) !important;
background: #ff0055 !important;
color: #000000 !important;
box-shadow: 0 0 10px rgba(255, 0, 85, 0.6) !important;
}
.br34-card-bookmark.saved svg {
fill: currentColor !important;
}
/* Dock Bookmark Badge */
.br34-bookmark-badge {
position: absolute;
top: -4px;
right: -4px;
background: #ff0055;
color: #000000;
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
font-weight: 900;
padding: 1px 5px;
border-radius: 4px;
box-shadow: 0 0 8px rgba(255, 0, 85, 0.7);
line-height: 1.1;
}
/* Micro Toast */
.br34-toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%) translateY(20px);
z-index: 2147483647;
background: rgba(10, 7, 14, 0.96);
border: 1px solid #ff0055;
border-radius: 6px;
color: #ffffff;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
padding: 8px 18px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.9), 0 0 16px rgba(255, 0, 85, 0.4);
opacity: 0;
pointer-events: none;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
text-transform: uppercase;
}
.br34-toast.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* EROS Archive Modal */
.br34-archive-modal {
position: fixed;
bottom: 74px;
right: 24px;
z-index: 2147483646;
width: 380px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 90px);
background: rgba(10, 7, 14, 0.97);
border: 1px solid rgba(255, 0, 85, 0.45);
border-radius: 8px;
box-shadow: 0 16px 50px rgba(0, 0, 0, 0.95), 0 0 25px rgba(255, 0, 85, 0.22);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
display: flex;
flex-direction: column;
color: #e5e5eb;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
opacity: 0;
transform: translateY(12px) scale(0.98);
pointer-events: none;
transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.br34-archive-modal.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.br34-archive-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
background: rgba(16, 10, 22, 0.95);
border-bottom: 1px solid rgba(255, 0, 85, 0.25);
}
.br34-archive-tab {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: #9c97a8;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.06em;
padding: 8px 10px;
cursor: pointer;
text-align: center;
transition: all 0.15s ease;
}
.br34-archive-tab:hover {
color: #ffffff;
background: rgba(255, 0, 85, 0.08);
}
.br34-archive-tab.active {
color: #ff0055;
border-bottom-color: #ff0055;
background: rgba(255, 0, 85, 0.12);
}
.br34-archive-body {
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
max-height: calc(100vh - 200px);
}
.br34-archive-toolbar {
width: 100%;
margin-bottom: 4px;
}
.br34-btn-mark-sector {
width: 100%;
background: rgba(18, 12, 24, 0.9);
border: 1px solid rgba(255, 0, 85, 0.45);
border-radius: 5px;
color: #ff0055;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.06em;
padding: 8px 12px;
cursor: pointer;
transition: all 0.15s ease;
text-align: center;
}
.br34-btn-mark-sector:hover {
background: #ff0055;
color: #000000;
box-shadow: 0 0 12px rgba(255, 0, 85, 0.6);
}
.br34-btn-mark-sector.saved {
background: rgba(255, 0, 85, 0.2);
border-color: #ff0055;
color: #ffffff;
}
.br34-archive-empty {
text-align: center;
padding: 30px 15px;
color: #7d798a;
font-size: 10.5px;
line-height: 1.5;
}
.br34-archive-empty-title {
color: #ff0055;
font-weight: 800;
margin-bottom: 6px;
letter-spacing: 0.08em;
}
.br34-archive-empty-sub {
font-size: 9.5px;
}
.br34-archive-list,
.br34-video-archive-list {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.br34-archive-item {
display: flex;
align-items: center;
justify-content: space-between;
background: #0e0a14;
border: 1px solid rgba(255, 255, 255, 0.08);
border-left: 3px solid #ff0055;
border-radius: 4px;
padding: 7px 10px;
gap: 8px;
transition: border-color 0.15s ease;
}
.br34-archive-item:hover {
border-color: rgba(255, 0, 85, 0.4);
border-left-color: #ff0055;
}
.br34-archive-item.is-current {
border-color: #ff0055;
background: rgba(255, 0, 85, 0.08);
}
.br34-archive-item-main {
display: flex;
flex-direction: column;
gap: 3px;
overflow: hidden;
}
.br34-archive-item-title {
font-size: 11px;
font-weight: 800;
color: #f1f1f5;
letter-spacing: 0.04em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.br34-archive-item-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 9.5px;
color: #8c8998;
}
.br34-tag-page {
color: #ff0055;
font-weight: 800;
}
.br34-archive-item-actions {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.br34-btn-jump {
background: #ff0055;
border: 1px solid #ff0055;
border-radius: 3px;
color: #000000;
font-family: 'JetBrains Mono', monospace;
font-size: 9.5px;
font-weight: 900;
padding: 3px 8px;
text-decoration: none;
cursor: pointer;
transition: all 0.12s ease;
}
.br34-btn-jump:hover {
background: #ffffff;
border-color: #ffffff;
color: #000000;
}
.br34-btn-del {
background: transparent;
border: 1px solid rgba(255, 0, 85, 0.3);
border-radius: 3px;
color: #ff0055;
font-family: 'JetBrains Mono', monospace;
font-size: 9.5px;
font-weight: 800;
padding: 3px 6px;
cursor: pointer;
transition: all 0.12s ease;
}
.br34-btn-del:hover {
background: #ff0055;
color: #000000;
}
.br34-video-archive-card {
display: flex;
align-items: center;
background: #0e0a14;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 4px;
padding: 6px;
gap: 8px;
transition: border-color 0.15s ease;
}
.br34-video-archive-card:hover {
border-color: rgba(255, 0, 85, 0.45);
}
.br34-video-archive-thumb-wrap {
position: relative;
width: 58px;
height: 38px;
flex-shrink: 0;
border-radius: 2px;
overflow: hidden;
background: #050307;
}
.br34-video-archive-thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.br34-video-archive-dur {
position: absolute;
bottom: 2px;
right: 2px;
background: rgba(0, 0, 0, 0.85);
color: #ffffff;
font-size: 8px;
font-weight: 700;
padding: 1px 3px;
border-radius: 2px;
line-height: 1;
}
.br34-video-archive-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.br34-video-archive-title {
font-size: 10.5px;
font-weight: 700;
color: #e5e5eb;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.br34-video-archive-title:hover {
color: #ff0055;
}
.br34-video-archive-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 9px;
color: #8c8998;
}
.br34-video-archive-rating {
color: #00e676;
font-weight: 700;
}
.br34-video-archive-views {
color: #a5a2b3;
}
.br34-video-archive-actions {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.br34-archive-footer {
padding: 8px 12px;
border-top: 1px solid rgba(255, 0, 85, 0.25);
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
background: rgba(16, 10, 22, 0.95);
}
.br34-archive-action-btn {
flex: 1;
background: rgba(18, 12, 24, 0.9);
border: 1px solid rgba(255, 0, 85, 0.35);
border-radius: 4px;
color: #e5e5eb;
font-family: 'JetBrains Mono', monospace;
font-size: 9.5px;
font-weight: 800;
padding: 5px 4px;
cursor: pointer;
text-align: center;
transition: all 0.12s ease;
white-space: nowrap;
}
.br34-archive-action-btn:hover {
background: #ff0055;
color: #000000;
}
.br34-archive-action-btn.danger {
border-color: rgba(255, 0, 85, 0.6);
color: #ff0055;
}
.br34-archive-action-btn.danger:hover {
background: #ff0055;
color: #000000;
}
/* ==========================================================================
Mobile Specific
========================================================================== */
@media (max-width: 480px) {
.br34-dock {
bottom: 16px;
right: 16px;
}
.br34-bookmark-btn {
height: 38px;
width: 38px;
}
.br34-fab {
bottom: 16px;
right: 16px;
height: 38px;
padding: 0 12px;
font-size: 11px;
}
.br34-panel,
.br34-archive-modal {
right: 10px;
bottom: 60px;
width: calc(100vw - 20px);
max-width: 340px;
}
.br34-panel-header {
padding: 8px 12px;
}
.br34-panel-body,
.br34-archive-body {
padding: 10px 12px;
gap: 10px;
}
.br34-sliders-vertical {
gap: 7px;
}
.br34-chip {
font-size: 9.5px;
padding: 6px 2px;
}
.br34-panel-footer,
.br34-archive-footer {
padding: 8px 12px;
}
}
`;
var managedCards = [];
var filterBar = null;
var autoPager = null;
var currentFilter = null;
var bookmarkHandle = null;
var lastActiveSort = null;
var awaitingAjaxReload = false;
function injectStyles() {
if (document.getElementById("br34-styles")) return;
const style = document.createElement("style");
style.id = "br34-styles";
style.textContent = CSS;
(document.head || document.documentElement).append(style);
}
function applyOwnWatched(el, data) {
if (data.id && isWatchedId(data.id)) {
data.isWatched = true;
el.classList.add("watched");
}
}
function scanCards() {
const container = findVideosContainer();
if (!container) return;
cleanAds(container);
const existingMap = new Map();
for (const c of managedCards) existingMap.set(c.el, c);
const updatedCards = [];
const cardElements = Array.from(container.querySelectorAll(".item.thumb"));
attachCardBookmarkButtons(cardElements, () => {
bookmarkHandle?.refresh();
});
for (const el of cardElements) {
if (isAdCard(el)) {
el.remove();
continue;
}
const existing = existingMap.get(el);
if (existing) {
applyOwnWatched(existing.el, existing.data);
updatedCards.push(existing);
continue;
}
const data = extractCardData(el);
if (data) {
applyOwnWatched(el, data);
updatedCards.push({
el,
data
});
}
}
managedCards = updatedCards;
}
function applyFilter() {
if (!currentFilter) return;
let visibleCount = 0;
for (const card of managedCards) {
const isVisible = matchesClientFilter(card.data, currentFilter);
card.el.dataset.br34Hidden = isVisible ? "false" : "true";
if (isVisible) visibleCount++;
}
filterBar?.setCount(visibleCount, managedCards.length);
}
function syncUrlSort(sortBy) {
try {
const url = new URL(window.location.href);
if (sortBy) url.searchParams.set("sort_by", sortBy);
else url.searchParams.delete("sort_by");
for (const k of [...url.searchParams.keys()]) if (isPaginationKey(k)) url.searchParams.delete(k);
url.pathname = stripPageSegment(url.pathname);
window.history.replaceState(window.history.state, "", url.toString());
} catch {}
}
function handleSortOrFilterReload() {
const currentSort = readActiveSort(document);
syncUrlSort(currentSort);
lastActiveSort = currentSort;
unclipBodyOverflow();
cleanAds();
initNativeFilterPanel();
managedCards = [];
scanCards();
applyFilter();
autoPager?.reset();
bookmarkHandle?.refresh();
}
function boot() {
injectStyles();
unclipBodyOverflow();
cleanAds();
hardenAnchorsIn(document);
initNewTab(document);
if (!isListingPage()) {
let dock = document.querySelector(".br34-dock");
if (!dock) {
dock = document.createElement("div");
dock.className = "br34-dock";
document.body.append(dock);
}
const dummyFab = document.createElement("div");
dummyFab.style.display = "none";
bookmarkHandle = mountBookmarkButton({
fab: dummyFab,
listKey: canonicalListKey(window.location.href),
getPage: () => 1,
getUrl: () => window.location.href
});
return;
}
if (!findVideosContainer()) return;
initNativeFilterPanel();
lastActiveSort = readActiveSort(document);
const listKey = canonicalListKey(window.location.href);
if (!filterBar || !filterBar.fabElement.isConnected) {
filterBar?.destroy();
filterBar = new FilterBar({ onFilterChange: (state) => {
currentFilter = state;
applyFilter();
} });
currentFilter = filterBar.getState();
}
scanCards();
if (!autoPager) {
autoPager = new AutoPager({
onNewCards: (newEls) => {
attachCardBookmarkButtons(newEls, () => {
bookmarkHandle?.refresh();
});
for (const el of newEls) {
for (const a of el.querySelectorAll("a[href*=\"/video/\"]")) hardenAnchor(a);
const data = extractCardData(el);
if (data) {
applyOwnWatched(el, data);
managedCards.push({
el,
data
});
} else el.remove();
}
applyFilter();
},
onPageLoaded: () => {
cleanAds();
unclipBodyOverflow();
initNativeFilterPanel();
bookmarkHandle?.refresh();
}
});
autoPager.init();
}
applyFilter();
if (filterBar && autoPager) {
const pager = autoPager;
bookmarkHandle = mountBookmarkButton({
fab: filterBar.fabElement,
listKey,
getPage: () => pager.getCurrentPage(),
getUrl: () => pager.getCurrentPageUrl()
});
}
}
var scheduledTimer = 0;
function scheduleScan() {
window.clearTimeout(scheduledTimer);
scheduledTimer = window.setTimeout(() => {
const sortChanged = readActiveSort(document) !== lastActiveSort;
if (awaitingAjaxReload || sortChanged) {
awaitingAjaxReload = false;
handleSortOrFilterReload();
return;
}
unclipBodyOverflow();
cleanAds();
initNativeFilterPanel();
scanCards();
applyFilter();
}, 100);
}
document.addEventListener("click", (e) => {
const target = e.target;
if (!target) return;
if (target.closest(".filters-panel a[data-action=\"ajax\"], .filters-panel .duration-filter__apply, [data-container-id*=\"sort_list\"], #js-ajax_sort, #js-ajax_sort_custom")) awaitingAjaxReload = true;
}, true);
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => {
boot();
});
else boot();
window.addEventListener("load", () => {
unclipBodyOverflow();
boot();
});
new MutationObserver((mutations) => {
if (autoPager?.getIsAppending()) return;
let shouldScan = false;
for (const m of mutations) {
for (const node of m.addedNodes) if (node instanceof HTMLElement) {
if (node.classList.contains("item") || node.querySelector?.(".item.thumb")) {
shouldScan = true;
break;
}
}
if (shouldScan) break;
}
if (shouldScan) scheduleScan();
}).observe(document.body || document.documentElement, {
childList: true,
subtree: true
});
})();