Sleazy Fork is available in English.
Explore grid and context-preserving Reels queue for ReelsMunkey
// ==UserScript==
// @name ReelsMunkey Enhanced
// @namespace https://reelsmunkey.com/
// @version 0.3.1
// @description Explore grid and context-preserving Reels queue for ReelsMunkey
// @match https://reelsmunkey.com/*
// @match https://www.reelsmunkey.com/*
// @run-at document-idle
// @grant none
// @noframes
// @license MIT
// ==/UserScript==
(() => {
'use strict';
const SITE = 'https://reelsmunkey.com';
const IMAGE_SITE = 'https://imgs.reelsmunkey.com/';
const PAGE_SIZE = 20;
const REQUEST_TIMEOUT = 15000;
const PREF_KEY = 'rm-enhanced-prefs-v1';
const TEST = typeof globalThis !== 'undefined' && globalThis.__RM_TEST__;
function videoKey(item) {
return String(item?._id || item?.url || '').trim();
}
function normalizeVideo(raw) {
if (!raw || typeof raw !== 'object') return null;
const source = raw.video && typeof raw.video === 'object' ? raw.video : raw;
let slug = source.url || source.slug || source.permalink || '';
if (typeof slug === 'string' && slug.startsWith('/video/')) slug = slug.slice(7).split(/[?#]/)[0];
if (typeof slug !== 'string' || !/^[\p{L}\p{N}_-]+$/u.test(slug)) return null;
const tags = Array.isArray(source.tags) ? source.tags.map(tag => ({
tag: String(tag?.tag || tag?.name || ''),
url: String(tag?.url || tag?.slug || '')
})) : [];
return {
_id: source._id ? String(source._id) : '',
url: slug,
title: String(source.title || 'Untitled video'),
description: String(source.description || ''),
thumbnail: typeof source.thumbnail === 'string' ? source.thumbnail : '',
created_at: typeof source.created_at === 'string' ? source.created_at : '',
views: Number(source.views?.count ?? source.views ?? NaN),
likes: Number(source.likes?.count ?? source.likes ?? NaN),
comments: Number(source.comments?.count ?? source.comments ?? NaN),
tags
};
}
function dedupe(items) {
const output = [];
const seen = new Set();
for (const raw of items) {
const item = normalizeVideo(raw);
if (!item) continue;
const key = videoKey(item);
if (seen.has(key) || seen.has(`slug:${item.url}`)) continue;
seen.add(key);
seen.add(`slug:${item.url}`);
output.push(item);
}
return output;
}
function listingUrl(source, page) {
const p = Math.max(1, Number(page) || 1);
if (source.kind === 'category') return `/collections/${encodeURIComponent(source.category)}${p > 1 ? `/${p}` : ''}`;
if (source.kind === 'popular') return `/sortby/${source.sort}/${source.period}${p > 1 ? `/${p}` : ''}`;
return p > 1 ? `/page/${p}` : '/';
}
function sourceFromControls(controls) {
const query = String(controls.query || '').trim();
const category = String(controls.category || '');
const sort = String(controls.sort || 'site');
const period = String(controls.period || 'allTime');
if (query) return { kind: 'search', query, category, sort, period, fullScan: !!category || sort !== 'site' || period !== 'allTime' };
if (category) return { kind: 'category', category, sort, period, fullScan: sort !== 'site' || period !== 'allTime' };
if (sort !== 'site') return { kind: 'popular', sort, period, fullScan: false };
return { kind: 'home', sort, period, fullScan: period !== 'allTime' };
}
function sourceLabel(source) {
const base = source.kind === 'search' ? `Search: ${source.query}` :
source.kind === 'category' ? `Category: ${source.category}` :
source.kind === 'popular' ? `Most ${source.sort}` : 'Latest videos';
const category = source.kind === 'search' && source.category ? ` · ${source.category}` : '';
const ranking = source.fullScan && source.sort !== 'site' ? ` · Most ${source.sort}` : '';
const period = source.period !== 'allTime' ? ` · ${source.period}` : '';
return base + category + ranking + period;
}
function extractNextData(doc) {
const node = doc.querySelector('script#__NEXT_DATA__');
if (!node?.textContent) throw new Error('Page data was not found.');
const json = JSON.parse(node.textContent);
return json?.props?.pageProps || {};
}
function mergeVideo(brief, detail) {
if (!detail) return brief;
return {
...brief,
_id: brief._id || detail._id,
title: brief.title !== 'Untitled video' ? brief.title : detail.title,
description: brief.description || detail.description,
thumbnail: brief.thumbnail || detail.thumbnail,
created_at: Number.isFinite(Date.parse(brief.created_at)) ? brief.created_at : detail.created_at,
views: Number.isFinite(brief.views) ? brief.views : detail.views,
likes: Number.isFinite(brief.likes) ? brief.likes : detail.likes,
comments: Number.isFinite(brief.comments) ? brief.comments : detail.comments,
tags: brief.tags.length ? brief.tags : detail.tags
};
}
function rankComplete(items, source, now = Date.now()) {
let result = items.filter(item => source.kind !== 'search' || !source.category || item.tags.some(tag => tag.url === source.category));
const cutoff = periodCutoff(source.period, now);
if (cutoff !== null) {
result = result.filter(item => Number.isFinite(Date.parse(item.created_at)) && Date.parse(item.created_at) >= cutoff);
}
if (source.sort !== 'site') {
result = result.map((item, index) => ({ item, index }))
.sort((a, b) => (b.item[source.sort] - a.item[source.sort]) || a.index - b.index)
.map(entry => entry.item);
}
return result;
}
class Queue {
constructor(fetchPage, onChange = () => {}) {
this.fetchPage = fetchPage;
this.onChange = onChange;
this.source = null;
this.items = [];
this.index = -1;
this.page = 0;
this.hasMore = false;
this.loading = false;
this.error = '';
this.generation = 0;
this.controller = null;
this.pending = null;
this.isolated = false;
}
notify() { this.onChange(this); }
cancel() {
this.generation++;
this.controller?.abort();
this.controller = null;
this.pending = null;
this.loading = false;
}
async replace(source) {
this.cancel();
const generation = this.generation;
const controller = new AbortController();
this.controller = controller;
this.loading = true;
this.error = '';
this.notify();
try {
const result = await this.fetchPage(source, 1, controller.signal);
if (generation !== this.generation) return false;
this.source = source;
this.items = dedupe(result.items);
this.index = -1;
this.page = 1;
this.hasMore = !!result.hasMore;
this.isolated = false;
this.error = '';
return true;
} catch (error) {
if (generation === this.generation && error?.name !== 'AbortError') this.error = error.message || 'Results could not be loaded.';
return false;
} finally {
if (generation === this.generation) { this.loading = false; this.notify(); }
}
}
installComplete(source, items) {
this.cancel();
this.source = source;
this.items = dedupe(items);
this.index = -1;
this.page = 1;
this.hasMore = false;
this.isolated = false;
this.error = '';
this.notify();
}
installDirect(item) {
this.cancel();
this.source = { kind: 'direct' };
this.items = dedupe([item]);
this.index = this.items.length ? 0 : -1;
this.page = 1;
this.hasMore = false;
this.isolated = true;
this.error = '';
this.notify();
}
async loadMore() {
if (!this.hasMore || !this.source) return false;
if (this.pending) return this.pending;
const generation = this.generation;
const page = this.page + 1;
const controller = this.controller || new AbortController();
this.controller = controller;
this.loading = true;
this.error = '';
this.notify();
this.pending = (async () => {
try {
const result = await this.fetchPage(this.source, page, controller.signal);
if (generation !== this.generation) return false;
const before = this.items.length;
const seen = new Set(this.items.flatMap(item => [videoKey(item), `slug:${item.url}`]));
for (const item of dedupe(result.items)) {
if (seen.has(videoKey(item)) || seen.has(`slug:${item.url}`)) continue;
seen.add(videoKey(item)); seen.add(`slug:${item.url}`);
this.items.push(item);
}
if (result.hasMore && this.items.length === before) {
// The source can legitimately repeat entries, but a whole repeated page
// cannot advance the visible queue safely.
throw new Error('The next page contained no new results.');
}
this.page = page;
this.hasMore = !!result.hasMore;
return true;
} catch (error) {
if (generation === this.generation && error?.name !== 'AbortError') this.error = error.message || 'More results could not be loaded.';
return false;
} finally {
if (generation === this.generation) { this.loading = false; this.pending = null; this.notify(); }
}
})();
return this.pending;
}
select(index) {
if (index < 0 || index >= this.items.length) return false;
this.index = index;
this.notify();
return true;
}
async move(delta) {
const target = this.index + delta;
if (target < 0) return 'start';
while (target >= this.items.length && this.hasMore) {
if (!await this.loadMore()) return this.error ? 'error' : 'end';
}
if (target >= this.items.length) return 'end';
this.select(target);
return 'selected';
}
}
function periodCutoff(period, now) {
const days = { today: 1, week: 7, month: 30 };
return days[period] ? now - days[period] * 86400000 : null;
}
function knownOutsideFilters(item, source, cutoff) {
if (cutoff !== null) {
const date = Date.parse(item.created_at);
if (Number.isFinite(date) && date < cutoff) return true;
}
return source.kind === 'search' && !!source.category && item.tags.length > 0 &&
!item.tags.some(tag => tag.url === source.category);
}
function needsDetail(item, source) {
if (source.kind === 'search' && source.category && !item.tags.length) return true;
if (source.period !== 'allTime' && !Number.isFinite(Date.parse(item.created_at))) return true;
if (source.sort !== 'site' && !Number.isFinite(item[source.sort])) return true;
return false;
}
async function collectComplete(source, fetchPageFn, enrichFn, signal, onProgress = () => {}, now = Date.now()) {
const all = [];
const pageController = new AbortController();
const abortPages = () => pageController.abort();
signal?.addEventListener('abort', abortPages, { once: true });
if (signal?.aborted) abortPages();
const pending = new Map();
const schedule = page => {
if (!pending.has(page)) {
// Resolve errors into values so speculative pages can be cancelled at the end
// without leaving an unhandled rejection.
pending.set(page, Promise.resolve().then(() => fetchPageFn(source, page, pageController.signal))
.then(result => ({ result }), error => ({ error })));
}
};
let page = 1;
let hasMore = true;
schedule(page);
try {
while (hasMore) {
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
const { result, error } = await pending.get(page);
pending.delete(page);
if (error) throw error;
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
if (!Array.isArray(result?.items)) throw new Error('Source returned an unexpected page.');
all.push(...result.items);
hasMore = !!result.hasMore;
onProgress({ phase: 'pages', page, records: all.length });
if (hasMore && !result.items.length) throw new Error('Source reported more results but returned an empty page.');
if (hasMore) for (let next = page + 1; next <= page + 4; next++) schedule(next);
page++;
}
} finally {
pageController.abort();
signal?.removeEventListener('abort', abortPages);
}
const unique = dedupe(all);
const cutoff = periodCutoff(source.period, now);
const candidates = unique.filter(item => !knownOutsideFilters(item, source, cutoff));
const checked = new Array(candidates.length);
let done = 0;
onProgress({ phase: 'metadata', done, total: candidates.length, skipped: unique.length - candidates.length });
for (let start = 0; start < candidates.length; start += 2) {
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
await Promise.all(candidates.slice(start, start + 2).map(async (item, offset) => {
const full = needsDetail(item, source) ? await enrichFn(item, signal) : item;
if (needsDetail(full, source)) throw new Error(`Required metadata is missing for ${item.url}.`);
checked[start + offset] = full;
done++;
onProgress({ phase: 'metadata', done, total: candidates.length, skipped: unique.length - candidates.length });
}));
}
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
return { items: rankComplete(checked, source, now), scanned: unique.length, skipped: unique.length - candidates.length };
}
if (TEST) {
globalThis.__RM_CORE__ = { Queue, normalizeVideo, dedupe, listingUrl, sourceFromControls, sourceLabel, extractNextData, mergeVideo, rankComplete, collectComplete };
return;
}
if (!/^https?:\/\/(?:www\.)?reelsmunkey\.com$/.test(location.origin) || document.getElementById('rm-enhanced-host')) return;
const cachedPages = new Map();
const details = new Map();
const detailPending = new Map();
let detailActive = 0;
const detailWaiters = [];
const originalUrl = location.href;
const initialPath = location.pathname;
const initialProps = (() => { try { return extractNextData(document); } catch { return null; } })();
const tags = Array.isArray(initialProps?.tags) ? initialProps.tags : Array.isArray(initialProps?.allTags) ? initialProps.allTags : [];
const prefs = (() => {
const defaults = { muted: true, volume: 0.7, loop: true, autoAdvance: false, cardSize: 230 };
try { return { ...defaults, ...JSON.parse(localStorage.getItem(PREF_KEY) || '{}') }; }
catch { return defaults; }
})();
for (const raw of Array.isArray(initialProps?.data) ? initialProps.data : []) {
const item = normalizeVideo(raw);
if (item) details.set(item.url, item);
}
const savePrefs = () => { try { localStorage.setItem(PREF_KEY, JSON.stringify(prefs)); } catch {} };
async function request(url, signal, json = false) {
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
const timeout = new AbortController();
const timer = setTimeout(() => timeout.abort(), REQUEST_TIMEOUT);
const onAbort = () => timeout.abort();
signal?.addEventListener('abort', onAbort, { once: true });
try {
const response = await fetch(url, { credentials: 'same-origin', signal: timeout.signal, headers: { Accept: json ? 'application/json' : 'text/html' } });
if (!response.ok) throw new Error(`Request failed (${response.status}).`);
return json ? response.json() : response.text();
} catch (error) {
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
if (timeout.signal.aborted) throw new Error('Request timed out.');
throw error;
} finally {
clearTimeout(timer);
signal?.removeEventListener('abort', onAbort);
}
}
async function pageProps(url, signal) {
if (cachedPages.has(url)) return cachedPages.get(url);
const html = await request(url, signal);
const doc = new DOMParser().parseFromString(html, 'text/html');
const props = extractNextData(doc);
cachedPages.set(url, props);
if (cachedPages.size > 40) cachedPages.delete(cachedPages.keys().next().value);
return props;
}
async function fetchPage(source, page, signal) {
if (source.kind === 'search') {
const params = new URLSearchParams({ search: source.query, page: String(page) });
const response = await request(`/api/search/query?${params}`, signal, true);
if (!Array.isArray(response?.videos) || typeof response?.hasMore !== 'boolean') throw new Error('Search returned an unexpected response.');
return { items: response.videos, hasMore: response.hasMore };
}
const path = listingUrl(source, page);
const props = page === 1 && path === initialPath && Array.isArray(initialProps?.data) ? initialProps : await pageProps(path, signal);
if (!Array.isArray(props.data)) throw new Error('Listing data was not found.');
const count = Number(props.count);
const currentPage = Number(props.currentPage) || page;
return { items: props.data, hasMore: Number.isFinite(count) ? currentPage * PAGE_SIZE < count : props.data.length === PAGE_SIZE };
}
async function withDetailSlot(work, signal) {
if (detailActive >= 2) await new Promise((resolve, reject) => {
const waiter = { resolve, reject };
detailWaiters.push(waiter);
signal?.addEventListener('abort', () => {
const index = detailWaiters.indexOf(waiter);
if (index >= 0) detailWaiters.splice(index, 1);
reject(new DOMException('Cancelled', 'AbortError'));
}, { once: true });
});
if (signal?.aborted) throw new DOMException('Cancelled', 'AbortError');
detailActive++;
try { return await work(); }
finally { detailActive--; detailWaiters.shift()?.resolve(); }
}
function findDetail(props, slug) {
const candidates = [props.video, props.data, props.videoData, props.item, props.post];
for (const candidate of candidates) {
if (Array.isArray(candidate)) {
const found = candidate.find(item => item?.url === slug);
if (found) return normalizeVideo(found);
} else if (candidate && typeof candidate === 'object') {
const found = normalizeVideo(candidate);
if (found?.url === slug) return found;
}
}
return null;
}
async function enrich(item, signal) {
if (details.has(item.url)) return mergeVideo(item, details.get(item.url));
if (!detailPending.has(item.url)) {
const promise = withDetailSlot(async () => {
const props = await pageProps(`/video/${encodeURIComponent(item.url)}`, signal);
const detail = findDetail(props, item.url);
if (!detail) throw new Error(`Metadata unavailable for ${item.url}.`);
details.set(item.url, detail);
return detail;
}, signal).finally(() => detailPending.delete(item.url));
detailPending.set(item.url, promise);
}
return mergeVideo(item, await detailPending.get(item.url));
}
const host = document.createElement('div');
host.id = 'rm-enhanced-host';
document.body.append(host);
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
:host{all:initial;color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color:#f5f5f5}
*{box-sizing:border-box}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:default}
#app{position:fixed;inset:0;z-index:2147483647;display:flex;background:#000;color:#f5f5f5;pointer-events:auto}
#app.original{background:transparent;pointer-events:none}#app.original>*:not(#return){display:none}#return{display:none}
#app.original #return{display:block;position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom));pointer-events:auto;background:#fff;color:#000;border:1px solid #fff;border-radius:999px;padding:13px 18px;box-shadow:0 8px 32px #000a;font-weight:700}
aside{width:220px;flex:none;border-right:1px solid #242424;padding:28px 16px;display:flex;flex-direction:column;gap:24px;background:#000}
.brand{font-size:22px;font-weight:800;letter-spacing:-.06em}.brand b{color:#fff}.nav{display:flex;flex-direction:column;gap:6px}
.nav button{background:transparent;border:1px solid transparent;border-radius:11px;color:#c8c8c8;text-align:left;padding:13px;font-weight:650;min-height:46px}
.nav button:hover,.nav button.active{background:#171717;border-color:#282828;color:#fff}.spacer{flex:1}.native{border-color:#333!important}
main{min-width:0;flex:1;overflow:auto;scrollbar-color:#444 #000}#explore{max-width:1440px;margin:auto;padding:34px clamp(18px,3.5vw,56px) 90px}
h1{font-size:clamp(30px,4vw,48px);letter-spacing:-.055em;margin:0 0 7px;line-height:1.1}p{margin:0}.lead{color:#aaa;font-size:14px;margin-bottom:25px}
.controls{background:#0a0a0a;border:1px solid #292929;border-radius:16px;padding:17px;display:grid;grid-template-columns:minmax(180px,2fr) repeat(3,minmax(120px,1fr)) auto;gap:12px;align-items:end;position:sticky;top:10px;z-index:2}
label{display:flex;flex-direction:column;gap:6px;color:#aaa;font-size:12px;font-weight:700;min-width:0}input,select{height:44px;width:100%;min-width:0;background:#000;color:#fff;border:1px solid #3a3a3a;border-radius:9px;padding:0 12px;outline:none}
input:focus-visible,select:focus-visible,button:focus-visible{outline:2px solid #fff;outline-offset:2px}input[type=range]{accent-color:#fff}
.primary{height:44px;background:#fff;border:1px solid #fff;color:#000;border-radius:9px;padding:0 18px;font-weight:800;white-space:nowrap}.primary:hover{background:#ddd}
.meta{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:24px 0 17px;flex-wrap:wrap}.meta h2{margin:0;font-size:20px;letter-spacing:-.03em}.sub{color:#999;font-size:13px}
.grid-tools{display:flex;align-items:center;gap:9px;color:#aaa;font-size:12px;font-weight:700}.grid-tools button{width:44px;height:44px;border:1px solid #333;background:#111;border-radius:9px;color:#fff;font-size:22px;line-height:1}.grid-tools button:hover{background:#222}
.grid-tools input{width:112px;height:36px;padding:0;border:0;background:transparent}.grid-tools output{min-width:54px;text-align:right;color:#ddd;font-variant-numeric:tabular-nums}
#notice{min-height:0;color:#ccc;font-size:13px;margin:0 0 12px}#notice:empty{display:none}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--rm-card-size,230px)),1fr));gap:14px}
.card{display:block;text-align:left;color:#fff;background:#080808;border:1px solid #252525;border-radius:12px;overflow:hidden;padding:0;min-width:0;transition:transform .15s,border-color .15s}.card:hover{transform:translateY(-3px);border-color:#777}
.thumb{aspect-ratio:9/13;background:#111;position:relative;display:grid;place-items:center;color:#888}.thumb img{width:100%;height:100%;object-fit:cover;display:block}.thumb .play{position:absolute;right:10px;bottom:10px;background:#000b;border:1px solid #aaa;border-radius:50%;width:34px;height:34px;display:grid;place-items:center;color:#fff}
.caption{padding:12px;min-height:66px;font-size:13px;font-weight:650;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.state{padding:32px 14px;text-align:center;color:#aaa}.secondary{background:#111;border:1px solid #444;border-radius:9px;color:#fff;padding:11px 17px;font-weight:700;min-height:44px}#load{display:none;margin:25px auto}
#viewer{display:none;position:fixed;inset:0;background:#000;z-index:3;min-width:0}.viewing #viewer{display:block}.viewing aside,.viewing main{display:none}
.viewer-head{position:absolute;top:0;left:0;right:0;min-height:60px;display:flex;align-items:flex-start;justify-content:space-between;padding:14px 18px;z-index:5;pointer-events:none}
.viewer-head button{pointer-events:auto;background:#000c;border:1px solid #555;border-radius:999px;color:#fff;padding:10px 15px;min-height:42px;font-weight:700}.viewer-head button:hover{background:#222}
.viewer-body{width:min(100%,1600px);height:100dvh;margin:auto;padding:10px 18px;display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,min(640px,calc((100dvh - 20px)*.62))) minmax(0,1fr);gap:18px;align-items:end}
.stage{grid-column:2;grid-row:1;height:calc(100dvh - 20px);width:100%;position:relative;background:#050505;border:1px solid #222;border-radius:9px;overflow:hidden;box-shadow:0 16px 50px #000}
.gesture-layer{position:absolute;inset:0;z-index:1;pointer-events:none;touch-action:pan-x pinch-zoom;cursor:pointer}.gesture-layer.active{pointer-events:auto}
#frameHolder{width:100%;height:100%}iframe{width:100%;height:100%;border:0;background:#000}.overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:#000b;text-align:center;padding:28px;z-index:2}.overlay.show{display:flex}.overlay .primary{height:auto;min-height:48px}
.player-controls{position:absolute;bottom:0;left:0;right:0;padding:28px 12px 12px;background:linear-gradient(transparent,#000f);display:flex;align-items:center;gap:8px;z-index:3}
.player-controls button{border:1px solid #ffffff44;background:#0009;color:#fff;border-radius:50%;min-width:42px;height:42px;padding:0 8px;font-weight:800}.player-controls button:hover{background:#333}
.player-controls input[type=range]{height:28px;padding:0;border:0;box-shadow:none;background:transparent;accent-color:#fff;min-width:0}.seek{flex:1}.volume{width:62px!important;flex:none}.time{font-size:11px;white-space:nowrap;color:#eee;font-variant-numeric:tabular-nums}
.info{grid-column:1;grid-row:1;align-self:end;justify-self:end;width:min(100%,300px);padding-bottom:22px;min-width:0}.info-inner{padding:12px}.eyebrow{font-size:10px;font-weight:800;letter-spacing:.12em;color:#aaa;margin-bottom:12px}.eyebrow span{display:block;letter-spacing:normal;text-transform:none;font-size:12px;font-weight:500;margin-top:5px;line-height:1.4;overflow-wrap:anywhere}
.info h2{font-size:20px;line-height:1.28;letter-spacing:-.03em;margin:0 0 10px;overflow-wrap:anywhere}.info p{color:#c9c9c9;font-size:13px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
.tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.tags:empty{display:none}.tags span{border:1px solid #333;border-radius:999px;padding:5px 8px;color:#bdbdbd;font-size:11px}
.queue-state{min-height:18px;color:#ddd;font-size:12px;line-height:1.35;margin-top:12px}.options{display:flex;flex-wrap:wrap;gap:14px;color:#bbb;font-size:12px;margin-top:12px}.options label{display:flex;flex-direction:row;align-items:center;gap:7px;font-weight:500;font-size:12px}.options input{width:18px;height:18px;accent-color:#fff}
.actions{grid-column:3;grid-row:1;align-self:end;justify-self:start;width:70px;display:flex;flex-direction:column;align-items:center;gap:12px;padding-bottom:30px}
.rail-button{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;background:transparent;border:0;color:#fff;min-width:62px;min-height:62px;border-radius:11px}.rail-button:hover{background:#171717}.rail-button span{font-size:25px;line-height:1}.rail-button small{font-size:10px;color:#ccc}.rail-divider{height:1px;width:36px;background:#333;margin:3px 0}
.metric{display:flex;flex-direction:column;align-items:center;gap:2px;color:#ddd;font-variant-numeric:tabular-nums}.metric>span{font-size:24px;line-height:1.1}.metric strong{font-size:12px;color:#fff}.metric small{font-size:10px;color:#999}
@media(max-width:1100px){aside{width:76px;padding:18px 8px;align-items:center}.brand{font-size:15px}.brand span,.nav button span,.native span{display:none}.nav button{font-size:20px;text-align:center;width:52px;padding:9px}.controls{grid-template-columns:repeat(2,minmax(0,1fr));position:static}.controls label:first-child,.controls .primary{grid-column:1/-1}.viewer-body{gap:8px}.info h2{font-size:17px}}
@media(max-width:900px){
#viewer{--rm-sheet:calc(178px + env(safe-area-inset-bottom))}.viewer-head{top:env(safe-area-inset-top);padding:10px 12px}.viewer-head button{min-height:44px}
.viewer-body{display:block;width:100%;height:100dvh;padding:0}.stage{position:absolute;top:0;left:0;right:0;width:100%;height:calc(100dvh - var(--rm-sheet));border:0;border-radius:0;box-shadow:none}
.info{position:absolute;left:0;right:0;bottom:0;width:100%;height:var(--rm-sheet);padding:0;overflow:auto;background:#000;border-top:1px solid #252525}.info-inner{padding:12px 16px calc(10px + env(safe-area-inset-bottom))}.eyebrow{margin-bottom:5px}.eyebrow span{display:inline;margin-left:7px}.info h2{font-size:15px;margin-bottom:5px}.info p{font-size:12px;-webkit-line-clamp:2}.tags{margin-top:7px;flex-wrap:nowrap;overflow:hidden}.tags span{white-space:nowrap}.queue-state{margin-top:6px}.options{margin-top:7px}
.actions{position:absolute;right:10px;bottom:calc(var(--rm-sheet) + 72px);width:54px;gap:5px;padding:0;z-index:4}.rail-button{min-width:50px;min-height:50px;background:#000b;border:1px solid #ffffff33;border-radius:50%}.rail-button span{font-size:22px}.rail-button small{font-size:8px}.rail-divider{margin:2px 0}.metric{background:#000a;border-radius:9px;width:50px;padding:4px 2px}.metric>span{font-size:19px}.metric strong{font-size:10px}.metric small{font-size:8px}
.player-controls{padding:22px 8px 8px;gap:5px}.player-controls button{min-width:42px;height:42px}.volume{display:none}
}
@media(max-width:620px){
#app{display:block}aside{position:fixed;bottom:0;left:0;right:0;width:100%;height:calc(60px + env(safe-area-inset-bottom));z-index:3;flex-direction:row;padding:5px 8px calc(5px + env(safe-area-inset-bottom));border-right:0;border-top:1px solid #242424;background:#000}
.brand,.spacer{display:none}.nav{flex-direction:row;width:100%;justify-content:space-around}.nav button{width:auto;min-width:78px;font-size:13px}.nav button span,.native span{display:inline}.native{font-size:12px!important}
main{height:calc(100dvh - 60px - env(safe-area-inset-bottom))}#explore{padding:22px 12px 75px}.controls{grid-template-columns:repeat(2,minmax(0,1fr));position:static;padding:12px;gap:9px}.controls label:first-child{grid-column:1/-1}.controls .primary{grid-column:1/-1;min-height:48px}
.meta{align-items:flex-start}.grid-tools{width:100%;justify-content:flex-end}.grid-tools input{flex:1;max-width:160px}.grid{grid-template-columns:repeat(var(--rm-mobile-cols,2),minmax(0,1fr));gap:9px}.caption{font-size:12px;min-height:57px}.card{border-radius:9px}
.viewer-head button{font-size:12px;padding:9px 11px}.time{font-size:10px}.player-controls button{min-width:38px;height:38px}.actions{right:8px}
}
@media(max-height:620px) and (max-width:900px){#viewer{--rm-sheet:calc(132px + env(safe-area-inset-bottom))}.info p,.tags{display:none}.actions{bottom:calc(var(--rm-sheet) + 55px)}}
@media(max-height:500px) and (orientation:landscape) and (max-width:900px){
.viewer-body{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,min(420px,calc((100dvh - 12px)*.9))) minmax(0,1fr);gap:8px;padding:6px;height:100dvh;align-items:end}
.stage{grid-column:2;grid-row:1;position:relative;inset:auto;width:100%;height:calc(100dvh - 12px);border:1px solid #222;border-radius:7px}
.info{grid-column:1;grid-row:1;position:static;width:100%;height:auto;max-height:calc(100dvh - 80px);padding:0;overflow:auto;border:0;align-self:end;justify-self:end}.info-inner{padding:8px}.info h2{font-size:13px}.info p,.tags{display:none}.options{gap:8px}
.actions{grid-column:3;grid-row:1;position:static;width:52px;gap:2px;padding:0 0 8px;align-self:end;justify-self:start}.rail-button{min-width:42px;min-height:42px}.rail-button span{font-size:18px}.metric{width:42px;padding:2px}.metric>span{font-size:15px}.metric strong{font-size:9px}.metric small{display:none}
.player-controls{gap:3px;padding:16px 4px 5px}.player-controls button{min-width:33px;height:33px}.time{font-size:9px}
}
@media(prefers-reduced-motion:reduce){.card{transition:none}}
</style>
<div id="app">
<aside><div class="brand">reels<b>munkey</b><span> / enhanced</span></div><div class="nav"><button id="navExplore" class="active">⌗ <span>Explore</span></button><button id="navTop">↗ <span>Top viewed</span></button></div><div class="spacer"></div><div class="nav"><button id="native" class="native">↙ <span>Original site</span></button></div></aside>
<main id="main"><section id="explore"><h1>Explore videos</h1><p class="lead">Find a video, then keep its exact result order as you watch.</p>
<form class="controls" id="filterForm"><label>Search<input id="query" type="search" placeholder="Search videos" autocomplete="off"></label><label>Category<select id="category"><option value="">All categories</option></select></label><label>Order<select id="sort"><option value="site">Site order</option><option value="views">Most viewed</option><option value="likes">Most liked</option><option value="comments">Most commented</option></select></label><label>Period<select id="period"><option value="allTime">All time</option><option value="today">Today / past 24h</option><option value="week">This week / past 7d</option><option value="month">This month / past 30d</option></select></label><button class="primary" id="apply" type="submit">Show results</button></form>
<div class="meta"><div><h2 id="heading">Latest videos</h2><div class="sub" id="summary"></div></div><div class="grid-tools"><span>Grid size</span><button id="smaller" type="button" aria-label="Smaller thumbnails">−</button><input id="cardSize" type="range" min="150" max="360" step="10" aria-label="Thumbnail size"><button id="larger" type="button" aria-label="Larger thumbnails">+</button><output id="cardSizeValue" for="cardSize"></output></div></div><div id="notice" role="status"></div><div id="grid" class="grid"></div><div id="empty" class="state" hidden>No results found.</div><button id="load" class="secondary">Load more</button>
</section></main>
<section id="viewer" aria-label="Video viewer"><div class="viewer-head"><button id="back">← Explore</button><button id="viewOriginal">Original video ↗</button></div><div class="viewer-body"><div class="info"><div class="info-inner"><div class="eyebrow">NOW PLAYING <span id="position"></span></div><h2 id="videoTitle"></h2><p id="videoDescription"></p><div id="videoTags" class="tags"></div><div class="queue-state" id="queueState" role="status"></div><div class="options"><label><input id="loop" type="checkbox">Loop</label><label><input id="advance" type="checkbox">Auto advance</label></div></div></div><div class="stage" id="stage"><div id="frameHolder"></div><div id="gestureLayer" class="gesture-layer" aria-hidden="true"></div><div class="overlay" id="playOverlay"><button class="primary" id="tapPlay">Tap to play</button></div><div class="player-controls"><button id="play" aria-label="Play or pause">▶</button><span class="time" id="time">0:00</span><input class="seek" id="seek" type="range" min="0" max="1000" value="0" aria-label="Seek"><button id="mute" aria-label="Mute or unmute">◖</button><input class="volume" id="volume" type="range" min="0" max="1" step="0.05" aria-label="Volume"><button id="fullscreen" aria-label="Fullscreen">⛶</button></div></div><div class="actions"><button id="previous" class="rail-button" aria-label="Previous result"><span aria-hidden="true">↑</span><small>Previous</small></button><button id="next" class="rail-button" aria-label="Next result"><span aria-hidden="true">↓</span><small>Next</small></button><div class="rail-divider"></div><div class="metric"><span aria-hidden="true">◉</span><strong id="viewCount">—</strong><small>views</small></div><div class="metric"><span aria-hidden="true">♡</span><strong id="likeCount">—</strong><small>likes</small></div><div class="metric"><span aria-hidden="true">◌</span><strong id="commentCount">—</strong><small>comments</small></div></div></div></section>
<button id="return">Return to Enhanced</button>
</div>`;
const $ = id => root.getElementById(id);
const ui = Object.fromEntries(['app','main','grid','heading','summary','notice','empty','load','query','category','sort','period','apply','cardSize','cardSizeValue','smaller','larger','viewer','frameHolder','stage','gestureLayer','playOverlay','tapPlay','play','seek','time','mute','volume','fullscreen','videoTitle','videoDescription','videoTags','position','viewCount','likeCount','commentCount','queueState','previous','next','loop','advance'].map(id => [id, $(id)]));
function populateTags(list) {
for (const tag of list) {
if (!tag?.url || !tag?.tag || Array.from(ui.category.options).some(option => option.value === tag.url)) continue;
const option = document.createElement('option');
option.value = tag.url;
option.textContent = tag.tag;
ui.category.append(option);
}
}
populateTags(tags);
ui.volume.value = String(prefs.volume);
ui.loop.checked = !!prefs.loop;
ui.advance.checked = !!prefs.autoAdvance;
function cardSizeValue() {
const size = Number(prefs.cardSize);
return Number.isFinite(size) ? Math.min(360, Math.max(150, Math.round(size / 10) * 10)) : 230;
}
function updateCardSize(size = cardSizeValue()) {
prefs.cardSize = Math.min(360, Math.max(150, Math.round(Number(size) / 10) * 10));
const columns = prefs.cardSize < 180 ? 3 : prefs.cardSize < 280 ? 2 : 1;
ui.app.style.setProperty('--rm-card-size', `${prefs.cardSize}px`);
ui.app.style.setProperty('--rm-mobile-cols', String(columns));
ui.cardSize.value = String(prefs.cardSize);
ui.cardSizeValue.textContent = window.matchMedia('(max-width:620px)').matches ? `${columns} col` : `${prefs.cardSize}px`;
savePrefs();
}
updateCardSize();
ui.cardSize.addEventListener('input', () => updateCardSize(ui.cardSize.value));
ui.smaller.addEventListener('click', () => updateCardSize(cardSizeValue() - 10));
ui.larger.addEventListener('click', () => updateCardSize(cardSizeValue() + 10));
window.addEventListener('resize', () => updateCardSize());
let mode = 'explore';
let scrollPosition = 0;
let renderedCount = 0;
let player = null;
let frame = null;
let frameGeneration = 0;
let frameObserver = null;
let wheelUntil = 0;
let wheelDistance = 0;
let wheelLastAt = 0;
let lastSwipeAt = 0;
let navigationPending = false;
let navigationToken = 0;
let scanController = null;
let scanGeneration = 0;
let nativeStartRoute = '';
const queue = new Queue(fetchPage, () => refresh());
function thumbUrl(filename) {
if (!filename || !/^[\w.-]+$/.test(filename)) return '';
return IMAGE_SITE + encodeURIComponent(filename);
}
function addCard(item, index) {
const button = document.createElement('button');
button.type = 'button'; button.className = 'card';
button.setAttribute('aria-label', `Open ${item.title}`);
const thumb = document.createElement('div'); thumb.className = 'thumb';
const image = thumbUrl(item.thumbnail);
if (image) {
const img = document.createElement('img'); img.src = image; img.alt = ''; img.loading = 'lazy';
thumb.append(img);
} else {
const placeholder = document.createElement('span'); placeholder.textContent = 'Preview unavailable'; thumb.append(placeholder);
enrich(item, queue.controller?.signal).then(full => {
if (full.thumbnail && queue.items[index]?.url === item.url) {
queue.items[index] = full;
const img = document.createElement('img'); img.src = thumbUrl(full.thumbnail); img.alt = ''; img.loading = 'lazy';
if (img.src) thumb.replaceChildren(img);
}
}).catch(() => {});
}
const play = document.createElement('span'); play.className = 'play'; play.textContent = '▶'; thumb.append(play);
const caption = document.createElement('div'); caption.className = 'caption'; caption.textContent = item.title;
button.append(thumb, caption);
button.addEventListener('click', () => openIndex(index));
ui.grid.append(button);
}
function renderGrid() {
if (renderedCount > queue.items.length) { ui.grid.replaceChildren(); renderedCount = 0; }
while (renderedCount < queue.items.length) { addCard(queue.items[renderedCount], renderedCount); renderedCount++; }
ui.empty.hidden = queue.loading || queue.items.length > 0;
ui.load.style.display = queue.hasMore && mode === 'explore' ? 'block' : 'none';
ui.load.disabled = queue.loading;
ui.load.textContent = queue.loading ? 'Loading…' : 'Load more';
}
function refresh() {
if (queue.source) ui.heading.textContent = queue.source.kind === 'direct' ? 'Direct video' : sourceLabel(queue.source);
ui.summary.textContent = `${queue.items.length} result${queue.items.length === 1 ? '' : 's'} loaded${queue.hasMore ? ' · More available' : ''}`;
ui.notice.textContent = queue.error || (queue.loading && mode === 'explore' ? 'Loading results…' : '');
renderGrid();
if (mode === 'viewer') updateViewerInfo();
}
function draft() { return { query: ui.query.value, category: ui.category.value, sort: ui.sort.value, period: ui.period.value }; }
function updateDraftNotice() {
if (scanController) { scanGeneration++; scanController.abort(); scanController = null; }
const source = sourceFromControls(draft());
ui.apply.textContent = source.fullScan ? 'Build complete list' : 'Show results';
if (source.fullScan) ui.notice.textContent = 'This combination requires every source page before filtering or ranking. Build complete list starts a cancellable scan.';
}
async function applyControls() {
const source = sourceFromControls(draft());
const generation = ++scanGeneration;
scanController?.abort();
scanController = null;
if (source.fullScan) {
queue.cancel();
scanController = new AbortController();
const signal = scanController.signal;
ui.apply.textContent = 'Cancel scan';
ui.notice.textContent = 'Building the complete matching list. This can take time; click Cancel scan to stop.';
try {
const result = await collectComplete(source, fetchPage, enrich, signal, progress => {
if (generation !== scanGeneration) return;
ui.notice.textContent = progress.phase === 'pages' ?
`Scanning source page ${progress.page} · ${progress.records} records. Click Cancel scan to stop.` :
`Checking metadata ${progress.done} / ${progress.total} · ${progress.skipped} already outside the selected filters. Click Cancel scan to stop.`;
});
if (generation !== scanGeneration || signal.aborted) return;
queue.installComplete(source, result.items);
resetGrid();
ui.notice.textContent = `Complete list built from ${result.scanned} source records; ${result.skipped} excluded before metadata checks.`;
} catch (error) {
if (generation === scanGeneration) ui.notice.textContent = error?.name === 'AbortError' ? 'Scan cancelled. Previous results are intact.' : `Scan failed: ${error.message} Previous results are intact.`;
} finally {
if (generation === scanGeneration) { scanController = null; ui.apply.textContent = source.fullScan ? 'Build complete list' : 'Show results'; }
}
return;
}
ui.apply.disabled = true;
const ok = await queue.replace(source);
if (generation === scanGeneration) {
ui.apply.disabled = false;
if (ok) { resetGrid(); ui.main.scrollTop = 0; }
}
}
function resetGrid() { ui.grid.replaceChildren(); renderedCount = 0; refresh(); }
function pauseOtherVideos() { for (const video of document.querySelectorAll('video')) { try { video.pause(); } catch {} } }
function disposePlayer() {
frameGeneration++;
frameObserver?.disconnect(); frameObserver = null;
if (player) { try { player.pause(); } catch {} }
player = null;
if (frame) { frame.src = 'about:blank'; frame.remove(); frame = null; }
ui.frameHolder.replaceChildren();
ui.gestureLayer.classList.remove('active');
ui.playOverlay.classList.remove('show');
ui.play.disabled = true; ui.seek.disabled = true;
}
function formatTime(seconds) {
if (!Number.isFinite(seconds)) return '0:00';
const n = Math.floor(seconds);
return `${Math.floor(n / 60)}:${String(n % 60).padStart(2, '0')}`;
}
function syncPlayerControls() {
if (!player) return;
ui.play.textContent = player.paused ? '▶' : 'Ⅱ';
ui.mute.textContent = player.muted ? '◖' : '◗';
ui.time.textContent = `${formatTime(player.currentTime)} / ${formatTime(player.duration)}`;
ui.seek.value = Number.isFinite(player.duration) && player.duration > 0 ? String(Math.round(player.currentTime / player.duration * 1000)) : '0';
}
function attachPlayer(video, generation) {
if (generation !== frameGeneration || player) return;
player = video;
ui.gestureLayer.classList.add('active');
for (const other of video.ownerDocument.querySelectorAll('video')) if (other !== video) { try { other.pause(); } catch {} }
video.muted = !!prefs.muted;
video.volume = Math.max(0, Math.min(1, Number(prefs.volume)));
video.loop = !!prefs.loop;
video.playsInline = true;
video.controls = false;
video.preload = 'metadata';
ui.play.disabled = false; ui.seek.disabled = false;
for (const event of ['timeupdate','durationchange','play','pause','volumechange']) video.addEventListener(event, syncPlayerControls);
video.addEventListener('ended', () => { if (!prefs.loop && prefs.autoAdvance && generation === frameGeneration) navigate(1); });
syncPlayerControls();
updateViewerInfo();
Promise.resolve(video.play()).then(() => ui.playOverlay.classList.remove('show')).catch(() => ui.playOverlay.classList.add('show'));
}
function prepareFrame(doc, generation) {
frameObserver?.disconnect(); frameObserver = null;
doc.addEventListener('keydown', onKeydown, true);
doc.addEventListener('wheel', onWheel, { passive: false, capture: true });
doc.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });
doc.addEventListener('touchend', onTouchEnd, { passive: true, capture: true });
doc.addEventListener('touchcancel', onTouchCancel, { passive: true, capture: true });
doc.addEventListener('play', event => { if (event.target?.tagName === 'VIDEO' && event.target !== player) event.target.pause(); }, true);
const tryAttach = () => {
if (generation !== frameGeneration) return;
if (player?.isConnected) return;
if (player) { try { player.pause(); } catch {} player = null; ui.gestureLayer.classList.remove('active'); }
const video = doc.querySelector('main video') || doc.querySelector('article video') || doc.querySelector('video');
if (!video) return;
const style = doc.createElement('style');
style.textContent = 'html,body{margin:0!important;width:100%!important;height:100%!important;overflow:hidden!important;background:#000!important}body>*{visibility:hidden!important}video{visibility:visible!important;position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;object-fit:contain!important;touch-action:pan-x!important;z-index:2147483647!important;background:#000!important}';
doc.head.append(style);
attachPlayer(video, generation);
};
tryAttach();
frameObserver = new MutationObserver(tryAttach);
frameObserver.observe(doc.documentElement, { childList: true, subtree: true });
setTimeout(() => {
if (generation === frameGeneration && !player) ui.queueState.textContent = 'Site player controls are shown in the frame. Playback controls here need a video element.';
}, 10000);
}
function loadPlayer(item) {
disposePlayer();
const generation = frameGeneration;
pauseOtherVideos();
ui.queueState.textContent = 'Loading the site player…';
frame = document.createElement('iframe');
frame.title = item.title;
frame.referrerPolicy = 'same-origin';
frame.allow = 'autoplay; fullscreen; picture-in-picture';
frame.src = `/video/${encodeURIComponent(item.url)}`;
frame.addEventListener('load', () => {
if (generation !== frameGeneration) return;
try { prepareFrame(frame.contentDocument, generation); }
catch { ui.queueState.textContent = 'The site player cannot be controlled here. Use “Open original video”.'; }
});
ui.frameHolder.append(frame);
setTimeout(() => {
if (generation === frameGeneration && !player) ui.queueState.textContent = 'If playback is unavailable here, open the original video.';
}, 13000);
}
function updateViewerInfo() {
const item = queue.items[queue.index];
if (!item) return;
ui.videoTitle.textContent = item.title;
ui.videoDescription.textContent = item.description;
ui.videoTags.replaceChildren();
for (const tag of item.tags.slice(0, 6)) {
if (!tag.tag) continue;
const chip = document.createElement('span');
chip.textContent = `#${tag.tag}`;
ui.videoTags.append(chip);
}
const countText = count => Number.isFinite(count) ? new Intl.NumberFormat().format(count) : '—';
ui.viewCount.textContent = countText(item.views);
ui.likeCount.textContent = countText(item.likes);
ui.commentCount.textContent = countText(item.comments);
ui.position.textContent = queue.isolated ? 'Direct link · No results queue' : `${queue.index + 1} of ${queue.items.length}${queue.hasMore ? '+' : ''} · ${sourceLabel(queue.source)}`;
ui.previous.disabled = queue.index <= 0;
ui.next.disabled = queue.isolated || (queue.index >= queue.items.length - 1 && !queue.hasMore);
if (queue.error) ui.queueState.textContent = `${queue.error} Try Next again to retry.`;
else if (queue.isolated) ui.queueState.textContent = 'Open Explore to create a browsing queue.';
else if (queue.index >= queue.items.length - 1 && !queue.hasMore) ui.queueState.textContent = 'End of results.';
else if (!queue.loading && player) ui.queueState.textContent = '';
}
function enrichCurrentItem() {
const index = queue.index;
const item = queue.items[index];
if (!item || (item.description && item.tags.length && Number.isFinite(item.views) && Number.isFinite(item.likes) && Number.isFinite(item.comments))) return;
enrich(item, queue.controller?.signal).then(full => {
if (queue.items[index]?.url !== item.url) return;
queue.items[index] = full;
if (mode === 'viewer' && queue.index === index) updateViewerInfo();
}).catch(() => {});
}
function enterViewer(push = true) {
const item = queue.items[queue.index];
if (!item) return;
scrollPosition = ui.main.scrollTop;
mode = 'viewer';
ui.app.classList.add('viewing');
if (push) history.pushState({ rmEnhanced: true, view: 'viewer', slug: item.url }, '', `${location.pathname}${location.search}#rm-reel=${encodeURIComponent(item.url)}`);
updateViewerInfo();
loadPlayer(item);
enrichCurrentItem();
}
function openIndex(index) { if (queue.select(index)) enterViewer(); }
function closeViewer(push = true) {
if (mode !== 'viewer') return;
navigationToken++; navigationPending = false;
if (queue.loading) queue.cancel();
disposePlayer();
mode = 'explore';
ui.app.classList.remove('viewing');
if (push) history.pushState({ rmEnhanced: true, view: 'explore' }, '', queue.isolated ? '/' : originalUrl);
ui.main.scrollTop = scrollPosition;
refresh();
if (queue.isolated) queue.replace({ kind: 'home', sort: 'site', period: 'allTime' }).then(ok => { if (ok) resetGrid(); });
}
async function navigate(delta) {
if (mode !== 'viewer' || queue.isolated || navigationPending) return;
navigationPending = true;
const token = ++navigationToken;
try {
const status = await queue.move(delta);
if (mode !== 'viewer' || token !== navigationToken) return;
if (status === 'selected') {
const item = queue.items[queue.index];
history.replaceState({ rmEnhanced: true, view: 'viewer', slug: item.url }, '', `${originalUrl.split('#')[0]}#rm-reel=${encodeURIComponent(item.url)}`);
updateViewerInfo();
loadPlayer(item);
enrichCurrentItem();
} else if (status === 'end') ui.queueState.textContent = 'End of results.';
else if (status === 'start') ui.queueState.textContent = 'Start of results.';
else if (status === 'error') ui.queueState.textContent = `${queue.error} Press Next to retry.`;
} finally { if (token === navigationToken) navigationPending = false; }
}
$('filterForm').addEventListener('submit', event => {
event.preventDefault();
if (scanController) { scanGeneration++; scanController.abort(); scanController = null; updateDraftNotice(); ui.notice.textContent = 'Scan cancelled. Previous results are intact.'; return; }
applyControls();
});
for (const control of [ui.query, ui.category, ui.sort, ui.period]) control.addEventListener(control === ui.query ? 'input' : 'change', updateDraftNotice);
ui.load.addEventListener('click', () => queue.loadMore());
ui.main.addEventListener('scroll', () => {
if (mode === 'explore' && !scanController && queue.hasMore && !queue.loading && ui.main.scrollHeight - ui.main.scrollTop - ui.main.clientHeight < 500) queue.loadMore();
}, { passive: true });
$('navExplore').addEventListener('click', () => { closeViewer(); ui.main.scrollTop = 0; });
$('navTop').addEventListener('click', () => { closeViewer(); ui.query.value = ''; ui.category.value = ''; ui.sort.value = 'views'; ui.period.value = 'today'; applyControls(); });
$('native').addEventListener('click', () => { nativeStartRoute = location.pathname + location.search; disposePlayer(); ui.app.classList.add('original'); });
$('return').addEventListener('click', () => {
if (location.pathname + location.search !== nativeStartRoute) { location.reload(); return; }
ui.app.classList.remove('original');
if (mode === 'viewer') loadPlayer(queue.items[queue.index]);
});
$('back').addEventListener('click', () => closeViewer());
$('viewOriginal').addEventListener('click', () => { const item = queue.items[queue.index]; if (item) window.open(`${SITE}/video/${encodeURIComponent(item.url)}`, '_blank', 'noopener'); });
ui.previous.addEventListener('click', () => navigate(-1));
ui.next.addEventListener('click', () => navigate(1));
ui.play.addEventListener('click', () => { if (!player) return; if (player.paused) player.play().then(() => ui.playOverlay.classList.remove('show')).catch(() => ui.playOverlay.classList.add('show')); else player.pause(); });
ui.tapPlay.addEventListener('click', () => player?.play().then(() => ui.playOverlay.classList.remove('show')).catch(() => {}));
ui.mute.addEventListener('click', () => { prefs.muted = !prefs.muted; if (player) player.muted = prefs.muted; savePrefs(); syncPlayerControls(); });
ui.volume.addEventListener('input', () => { prefs.volume = Number(ui.volume.value); prefs.muted = false; if (player) { player.volume = prefs.volume; player.muted = false; } savePrefs(); syncPlayerControls(); });
ui.seek.addEventListener('input', () => { if (player && Number.isFinite(player.duration)) player.currentTime = player.duration * Number(ui.seek.value) / 1000; });
ui.fullscreen.addEventListener('click', () => { if (document.fullscreenElement) document.exitFullscreen(); else ui.stage.requestFullscreen?.(); });
ui.loop.addEventListener('change', () => { prefs.loop = ui.loop.checked; if (player) player.loop = prefs.loop; savePrefs(); });
ui.advance.addEventListener('change', () => { prefs.autoAdvance = ui.advance.checked; savePrefs(); });
function onKeydown(event) {
if (mode !== 'viewer' || ui.app.classList.contains('original') || event.altKey || event.ctrlKey || event.metaKey) return;
const target = event.composedPath()[0];
if (target?.closest?.('input,textarea,select,button,[contenteditable="true"],[role="slider"]') || target?.isContentEditable) return;
const action = { ArrowDown: () => navigate(1), ArrowUp: () => navigate(-1), ' ': () => ui.play.click(), m: () => ui.mute.click(), M: () => ui.mute.click(), Escape: () => closeViewer() }[event.key];
if (action) { event.preventDefault(); action(); }
}
document.addEventListener('keydown', onKeydown, true);
function onWheel(event) {
if (mode !== 'viewer' || ui.app.classList.contains('original') || event.ctrlKey || event.target?.closest?.('.player-controls,input,textarea,select,button,[contenteditable="true"],[role="slider"]')) return;
const distance = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? window.innerHeight : 1);
if (!distance) return;
event.preventDefault();
const now = Date.now();
if (now < wheelUntil) { wheelDistance = 0; return; }
if (now - wheelLastAt > 240 || Math.sign(distance) !== Math.sign(wheelDistance)) wheelDistance = 0;
wheelLastAt = now;
wheelDistance += distance;
if (Math.abs(wheelDistance) < 80) return;
const direction = Math.sign(wheelDistance);
wheelDistance = 0;
wheelUntil = now + 850;
navigate(direction);
}
ui.viewer.addEventListener('wheel', onWheel, { passive: false });
let touchStart = null;
function onTouchStart(event) {
if (mode !== 'viewer' || ui.app.classList.contains('original') || event.touches?.length > 1 || event.target?.closest?.('.player-controls,input,textarea,select,button,[role="slider"]')) { touchStart = null; return; }
const touch = event.changedTouches[0];
if (touch) touchStart = { id: touch.identifier, x: touch.clientX, y: touch.clientY };
}
ui.stage.addEventListener('touchstart', onTouchStart, { passive: true });
function onTouchEnd(event) {
if (!touchStart) return;
const touch = Array.from(event.changedTouches).find(item => item.identifier === touchStart.id) || event.changedTouches[0];
if (!touch) { touchStart = null; return; }
const dx = touch.clientX - touchStart.x, dy = touch.clientY - touchStart.y;
touchStart = null;
if (Math.abs(dy) > 65 && Math.abs(dy) > Math.abs(dx) * 1.25) { lastSwipeAt = Date.now(); navigate(dy < 0 ? 1 : -1); }
}
ui.stage.addEventListener('touchend', onTouchEnd, { passive: true });
function onTouchCancel() { touchStart = null; }
ui.stage.addEventListener('touchcancel', onTouchCancel, { passive: true });
ui.gestureLayer.addEventListener('click', () => { if (Date.now() - lastSwipeAt > 450 && player) ui.play.click(); });
window.addEventListener('popstate', event => {
if (event.state?.rmEnhanced && event.state.view === 'viewer') {
const index = queue.items.findIndex(item => item.url === event.state.slug);
if (index >= 0) { queue.select(index); enterViewer(false); }
} else if (mode === 'viewer') closeViewer(false);
});
async function initialize() {
try {
if (!tags.length) pageProps('/').then(props => populateTags(props.tags || props.allTags || [])).catch(() => {});
const direct = /^\/video\/([^/]+)/.exec(initialPath);
if (direct) {
const slug = decodeURIComponent(direct[1]);
const candidate = findDetail(initialProps || {}, slug) || normalizeVideo({ url: slug, title: document.title || 'Video' });
queue.installDirect(candidate);
enterViewer(false);
return;
}
let source = { kind: 'home', sort: 'site', period: 'allTime' };
const category = /^\/collections\/([^/]+)(?:\/\d+)?\/?$/.exec(initialPath);
const popular = /^\/sortby\/(views|likes|comments)\/(today|week|month|allTime)(?:\/\d+)?\/?$/.exec(initialPath);
if (category) {
source = { kind: 'category', category: decodeURIComponent(category[1]), sort: 'site', period: 'allTime' };
if (!Array.from(ui.category.options).some(option => option.value === source.category)) populateTags([{ tag: source.category, url: source.category }]);
ui.category.value = source.category;
}
if (popular) { source = { kind: 'popular', sort: popular[1], period: popular[2] }; ui.sort.value = source.sort; ui.period.value = source.period; }
const success = await queue.replace(source);
if (success) resetGrid();
} catch (error) {
ui.notice.textContent = `Enhanced view could not start: ${error.message}. Use Original site below.`;
}
}
initialize();
})();