NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Erome Ultimate Premium — By SuperNinja
// @namespace https://github.com/supernin/erome-ultimate
// @version 7.1.0
// @description The ultimate all-in-one Erome enhancer: TikTok-style vertical feed with Erome-native buttons (Like, Favorite, Comment, Share, Report), background album preloading, downloads (single + bulk ZIP), M3U8/HLS, sort by views/video-count/photo-count, infinite scroll, video-only mode, cinema mode, playback speed, flip video, PiP, NSFW blur, hide photos/videos, album counts, video duration badges, like counts, hidden-by-duration, redirect/popup blocker, modern Twitter-like popup UI.
// @author SuperNinja
// @icon https://www.erome.com/favicon-32x32.png
// @match https://*.erome.com/*
// @match https://www.erome.com/*
// @run-at document-end
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_download
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @connect erome.com
// @connect *.erome.com
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @license MIT
// ==/UserScript==
/* globals $ JSZip saveAs */
(function () {
'use strict';
/* ============================================================
* CONFIGURATION & STATE
* ============================================================ */
const CONFIG = {
brand: 'Erome Ultimate',
version: '7.1.0',
accent: '#8a5acc',
accentSoft: '#b39ad6',
dark: '#14151f',
darker: '#0e0f17',
surface: '#1d1e2a',
success: '#4ade80',
danger: '#ef4444',
warning: '#fbbf24'
};
/* === Embedded Erome Logo (base64 PNG) === */
const LOGO_SPLASH = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAACcCAMAAABm1A0xAAAB/lBMVEWcHy2hV1ceFCBUICHc2uCcKE3PV2+WatIQEB2tkY7Ms9kPkpPgJGHbqKqHSzqhjnJgM1jcX4yOcpq3nd49QD3rxK1tU5nCCSZd196o9fclYmUp0tXWmHRUPoB+R8fmPYV7cF+FPrwFBQoRERsUEiN2dnKsCCNWVlM2NTS0hnDGyshJSEksKSZoaGlMOStxWEbS1tDMlnjn7OOMZ1JWRTe1uLaPhXOuemrSmYiUlpTlp47WpInl59tyZFOHiYgAAAHqt5Tv9O6OdFqnqKjFiXJpSTeOemcDAwUBAQMtIxuvlXgCAgSKWUnHxrm9wsJvUj2SBhgUDyCsdVpTR2mwpY+9wrsqGhOmAhvd4desmod5eYMGBgwEBAinalZFOlVwOS90Z41bWWZkWnHNua1IKhp0CBK6vMHToX07OUN9gYOIVsyxaWUGBgs+QUHhnIvUtZSdoaLuSZLo2NBeYWKbnKHGeXFxKCrJi4htTUFuWpGqh9ldYFlvFxl9gXusNVDV/f0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS69gxAAAAgHRSTlP//+n//////6z//////////////////////////////////v7+//////////////////////////////////8F/////////43P//+x////////////////////US7///////////////////9w/////////////////////////+lpkHUAABqCSURBVHja7d2LX9NY9gBwanmsDr7H2dn375cQkzS5Sds0SUNJVvKaaadS7SBYdpGnCjK+38/913/n3PSRloIocX9lN0cUFHTmky/nnHtuHh37e2LxzyHxj6HxWyd+6o/f/fQ7jF/gjQZ+FMWpfTFzhGAPC2Yk47ex/kPyS1/8PDQS/5847LDNHC9OnMhvY6Pz/xI7UEnoHAwxyiIjCMIklywHpsboivw0NmIew7Mj4ao1wiKjAxJlxjCPmWPHgSIMm4Icnh/st/AY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';
const LOGO_ICON = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAB/lBMVEWXHiq1mNve4ttgIxyXbNPQcJp1WZuDTzyhUk2fjXHoIVrDp+Day7MQEBs/QDyvK0OtqZvEYGjgn34fIB45LEVfUjxjT2KHY7bABCPCpq4MCxUREBwUEiNVVlQ3NzRISEjGycezhnByWEfNl3mtCCMsKSZnaGmKZ1LR19CuemlLOStTRDd1dXLn7ONwZFWNhnTRmYe0t7Xlp4+HiIjYpIlmSjjk5tuVl5Tqt5OnqaeOdFqNe2fr8uqvlHbEiHEvIxuMWkoUDiDHxrptUTypARurdlu0pI2SBRi9wsJ5eIMJCREICBB0aYusmIcqGxNDOlBlWnKmali9wrnd4dhQQ2c9OkVxOjA+QUFeYWLNuLIQEB2doaLa4+Pm2s59gYS5vMLIioVdYFvinYtcV2R3BxNsFxl0KCWal6S0Z2W1jIaUaNWrFy2rJzbHe2nXtI99gHuwQ1O1WWiqh9dFKBdvWZKWISuUbGOCeomzOETJnKXQoX3Mu8jjxLgkHS5HLSdtTUIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+vwaQAAAAgHRSTlP/////////////////v/////////////////7+/v///////////////////////////////////////////////////////////7nR///////////////////T/////////////////////////////////////////////////zEiVIkAAArlSURBVHja7Zn3X9tIGocNJJtsL9elqMwISVZBxbIQkpHtuGHvYlpCCwRISAKk92w2u5t//d4ZGWODDVifu/vpvsTYOPb76C3zzowm83NX3377y507d34hupOIBd04T+wZMaeVSZ7OfvLGxWIHaQigAzlFGo2RfHkogD1l/8Zl1Yu4AJDKfh9iAKEPkMp8L4I5S+gBnDgwNIMD1Uc4k4bMafsM/cbpnF1EGO5CZmCAzi0/Vu5XP+G0C5mTEhpwQSBioiTLPV/5C39K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';
const STATE = {
showPhotos: true,
showVideos: true,
showDownloadButtons: true,
cinemaMode: false,
nsfwBlur: loadBool('eu_nsfw', false),
hiddenSeconds: 0,
videoOnlyMode: loadBool('eu_video_only', false),
sortAscending: false,
sortMode: 'views', // 'views' | 'videos' | 'photos'
videoDurations: new Map(),
downloadQueue: [],
downloadInProgress: false,
tiktokMode: false,
tiktokType: 'videos', // 'videos' | 'photos' | 'all'
tiktokIndex: 0
};
function loadBool(key, def) {
try { return localStorage.getItem(key) === 'true' ? true : (localStorage.getItem(key) === 'false' ? false : def); }
catch (e) { return def; }
}
function saveBool(key, val) {
try { localStorage.setItem(key, val ? 'true' : 'false'); } catch (e) {}
}
const IS_ALBUM_PAGE = /^\/a\//.test(window.location.pathname);
/* ============================================================
* REDIRECT & POPUP BLOCKER
* ============================================================ */
const BLOCKED_PATTERNS = [
'brightadnetwork.com',
'pemsrv.com',
'/jump/next.php',
'splash.php'
];
function isBlockedUrl(url) {
return url && typeof url === 'string' && BLOCKED_PATTERNS.some(p => url.includes(p));
}
(function installRedirectBlocker() {
const origOpen = window.open;
window.open = function (url, ...rest) {
if (isBlockedUrl(url)) { console.log('[EU] Blocked popup:', url); return null; }
return origOpen.apply(this, [url, ...rest]);
};
document.addEventListener('click', function (e) {
let t = e.target;
while (t && t.tagName !== 'A' && t.tagName !== 'FORM') t = t.parentElement;
if (t && t.tagName === 'A' && isBlockedUrl(t.href)) {
console.log('[EU] Blocked click redirect:', t.href);
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
}
}, { capture: true, passive: false });
const observer = new MutationObserver(muts => {
muts.forEach(m => m.addedNodes.forEach(node => {
if (node.tagName === 'A' && isBlockedUrl(node.href)) { node.href = '#'; node.onclick = e => { e.preventDefault(); return false; }; }
if ((node.tagName === 'SCRIPT' || node.tagName === 'IFRAME') && isBlockedUrl(node.src)) node.remove();
}));
});
observer.observe(document.documentElement, { childList: true, subtree: true });
})();
/* ============================================================
* STYLES — Modern Twitter-like UI
* ============================================================ */
GM_addStyle(`
/* ---------- Global Theme ---------- */
body {
background-color: ${CONFIG.surface} !important;
color: #fff;
}
a { color: ${CONFIG.accent}; }
/* ---------- Navbar ---------- */
.navbar-inverse { background-color: ${CONFIG.dark} !important; }
.nav.navbar-nav.navbar-right li a {
color: ${CONFIG.accent};
transition: all .2s ease-in-out;
}
.nav.navbar-nav.navbar-right li a:hover {
color: #fff;
text-shadow: 0 0 10px rgba(138,90,204,.9);
transform: scale(1.05);
}
/* ---------- Buttons ---------- */
.btn-pink {
color: #fff;
background-color: rgba(138,90,204,.35);
border-radius: 6px;
border: 1px solid transparent;
transition: all .25s ease;
}
.btn-pink:hover {
background-color: transparent;
border: 1px solid ${CONFIG.accent} !important;
box-shadow: 0 0 20px 2px rgba(138,90,204,.5);
color: #fff;
}
.btn-grey {
color: #fff;
border-radius: 6px;
filter: drop-shadow(0 0 5px rgba(138,90,204,.6));
}
.btn-grey:hover {
background: transparent !important;
border: 1px solid ${CONFIG.accent} !important;
box-shadow: 0 0 15px rgba(138,90,204,.5);
}
/* ---------- Album Cards ---------- */
.album-thumbnail-container {
transition: all .25s ease-in-out;
position: relative;
border-radius: 8px;
overflow: hidden;
}
.album-thumbnail-container:hover {
transform: scale(.96);
box-shadow: 0 0 25px 4px rgba(138,90,204,.55);
}
.album .album-thumbnail {
object-fit: cover;
width: 100%;
height: 100%;
}
/* ---------- Pagination ---------- */
.pagination > li > a, .pagination > li > span {
background-color: ${CONFIG.dark};
color: #fff;
border: none;
border-radius: 6px;
margin: 0 4px;
transition: background-color .3s ease;
}
.pagination > li > a:hover, .pagination > .active > a,
.pagination > .active > span {
background-color: ${CONFIG.accent};
color: #fff;
box-shadow: 0 0 10px rgba(138,90,204,.7);
}
/* ---------- NSFW Blur ---------- */
.eu-blur {
filter: grayscale(100%) blur(10px) invert(100%) hue-rotate(90deg) contrast(170%);
transition: filter .3s ease;
}
/* ---------- Keyframes ---------- */
@keyframes eu-spin { to { transform: rotate(360deg); } }
@keyframes eu-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.08); } }
@keyframes eu-fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes eu-toast-in {
0% { opacity: 0; transform: translateX(100px); }
100% { opacity: 1; transform: translateX(0); }
}
/* ---------- Download Button on Media ---------- */
.eu-dl-btn {
position: absolute;
top: 10px; left: 10px;
width: 40px; height: 40px;
border-radius: 50%;
background: ${CONFIG.dark};
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 3px rgba(138,90,204,.4);
cursor: pointer;
transition: all .3s ease;
overflow: hidden;
color: #fff;
font-size: 13px;
font-weight: 600;
z-index: 9999;
white-space: nowrap;
}
.eu-dl-btn svg { width: 16px; height: 16px; transition: margin-right .2s ease; }
.eu-dl-btn:hover {
width: 110px;
border-radius: 24px;
background: ${CONFIG.accent};
}
.eu-dl-btn:hover .eu-dl-label { display: inline; margin-left: 6px; }
.eu-dl-btn .eu-dl-label { display: none; }
.eu-dl-btn.downloading { background: ${CONFIG.accent}; width: 110px; border-radius: 24px; pointer-events: none; }
.eu-dl-btn.downloading .eu-dl-label { display: inline; margin-left: 6px; }
/* ---------- Album Badges ---------- */
.eu-duration-badge {
position: absolute;
top: 8px; right: 8px;
background: rgba(0,0,0,.82);
color: #fff;
padding: 3px 7px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
z-index: 15;
backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,.08);
}
.eu-count-badge {
position: absolute;
top: 8px; left: 8px;
background: rgba(0,0,0,.82);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
z-index: 15;
display: flex;
gap: 8px;
backdrop-filter: blur(4px);
border: 1px solid rgba(255,255,255,.08);
}
.eu-likes-badge {
color: ${CONFIG.accent};
filter: drop-shadow(0 0 4px rgba(138,90,204,.8));
margin-left: 6px;
font-weight: 600;
}
/* ---------- Toast Notifications ---------- */
.eu-toast-wrap {
position: fixed;
bottom: 24px; right: 24px;
display: flex;
flex-direction: column-reverse;
gap: 10px;
z-index: 100000;
pointer-events: none;
}
.eu-toast {
display: flex;
align-items: center;
gap: 10px;
min-width: 240px;
max-width: 360px;
padding: 12px 16px;
border-radius: 12px;
background: rgba(20,21,31,.95);
color: ${CONFIG.accentSoft};
font-size: 13.5px;
font-family: 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,.4), 0 0 0 1px rgba(138,90,204,.25);
backdrop-filter: blur(10px);
animation: eu-toast-in .35s cubic-bezier(.22,1,.36,1);
border-left: 3px solid ${CONFIG.accent};
pointer-events: auto;
}
.eu-toast.success { border-left-color: ${CONFIG.success}; color: #e0ffef; }
.eu-toast.error { border-left-color: ${CONFIG.danger}; color: #ffe0e0; }
.eu-toast.warning { border-left-color: ${CONFIG.warning}; color: #fff4d6; }
.eu-toast svg { flex-shrink: 0; }
/* ---------- Hub Menu (Twitter-like popup cards) ---------- */
.eu-hub {
position: relative;
display: inline-block;
margin-left: 10px;
vertical-align: middle;
margin-top: 15px;
}
.eu-hub-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 9px;
background: ${CONFIG.surface};
border-radius: 10px;
color: ${CONFIG.accentSoft};
transition: all .3s ease;
cursor: pointer;
border: 1px solid transparent;
}
.eu-hub-btn:hover {
border-color: ${CONFIG.accent};
box-shadow: 0 0 15px rgba(138,90,204,.4);
transform: translateY(-1px);
}
.eu-hub-btn svg { width: 18px; height: 18px; fill: #fff; }
.eu-hub-menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
background: ${CONFIG.dark};
border-radius: 12px;
box-shadow: 0 12px 32px rgba(0,0,0,.6), 0 0 0 1px rgba(138,90,204,.2);
padding: 8px;
list-style: none;
display: none;
min-width: 220px;
z-index: 10000;
backdrop-filter: blur(12px);
animation: eu-fade-in .2s ease;
}
.eu-hub.open .eu-hub-menu { display: block; }
.eu-hub-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
cursor: pointer;
color: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
transition: all .2s ease;
white-space: nowrap;
}
.eu-hub-item:hover {
background: rgba(138,90,204,.15);
transform: translateX(3px);
}
.eu-hub-item svg { width: 16px; height: 16px; fill: ${CONFIG.accentSoft}; flex-shrink: 0; }
.eu-hub-item.inactive { opacity: .45; }
.eu-hub-item .eu-kbd {
margin-left: auto;
background: rgba(255,255,255,.08);
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
color: ${CONFIG.accentSoft};
}
/* ---------- Bulk Download Modal ---------- */
.eu-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.75);
backdrop-filter: blur(6px);
z-index: 99998;
display: none;
align-items: center;
justify-content: center;
animation: eu-fade-in .2s ease;
}
.eu-modal-overlay.open { display: flex; }
.eu-modal {
background: ${CONFIG.dark};
border-radius: 16px;
width: 440px;
max-width: 92vw;
max-height: 90vh;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0,0,0,.7), 0 0 0 1px rgba(138,90,204,.2);
animation: eu-fade-in .25s cubic-bezier(.22,1,.36,1);
}
.eu-modal-header {
padding: 18px 22px;
background: linear-gradient(135deg, ${CONFIG.accent}, #6b3fb0);
display: flex;
align-items: center;
justify-content: space-between;
}
.eu-modal-header h3 {
margin: 0;
color: #fff;
font-size: 17px;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
.eu-modal-close {
background: rgba(255,255,255,.2);
border: none;
color: #fff;
width: 30px; height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background .2s ease;
}
.eu-modal-close:hover { background: rgba(255,255,255,.35); }
.eu-modal-body { padding: 22px; }
.eu-option-group { margin-bottom: 18px; }
.eu-option-title {
color: ${CONFIG.accentSoft};
font-weight: 700;
font-size: 12px;
text-transform: uppercase;
letter-spacing: .8px;
margin-bottom: 10px;
}
.eu-option {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
color: #e4e4e8;
cursor: pointer;
transition: color .2s ease;
font-size: 14px;
}
.eu-option:hover { color: ${CONFIG.accentSoft}; }
.eu-option input { display: none; }
.eu-radio, .eu-check {
width: 18px; height: 18px;
border: 2px solid #555;
border-radius: 50%;
position: relative;
transition: all .25s ease;
flex-shrink: 0;
}
.eu-check { border-radius: 4px; }
.eu-option input:checked + .eu-radio,
.eu-option input:checked + .eu-check {
border-color: ${CONFIG.accent};
background: ${CONFIG.accent};
}
.eu-option input:checked + .eu-radio::after {
content: ''; position: absolute;
top: 4px; left: 4px;
width: 6px; height: 6px;
background: #fff; border-radius: 50%;
}
.eu-option input:checked + .eu-check::after {
content: '✓'; position: absolute;
top: -2px; left: 2px;
color: #fff; font-size: 13px; font-weight: bold;
}
.eu-zip-preview {
font-size: 11px;
color: #888;
margin-top: 6px;
padding: 8px 10px;
background: ${CONFIG.darker};
border-radius: 6px;
word-break: break-all;
font-family: monospace;
}
.eu-btn-primary {
width: 100%;
padding: 13px;
background: linear-gradient(135deg, ${CONFIG.accent}, #a77fd4);
color: #fff;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: all .25s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.eu-btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(138,90,204,.5);
}
.eu-btn-primary:disabled { opacity: .55; cursor: not-allowed; }
.eu-progress-box {
margin-top: 14px;
padding: 14px;
background: ${CONFIG.darker};
border-radius: 10px;
}
.eu-progress-bar {
width: 100%; height: 6px;
background: rgba(255,255,255,.08);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.eu-progress-fill {
height: 100%;
background: linear-gradient(90deg, ${CONFIG.accent}, ${CONFIG.accentSoft});
width: 0;
transition: width .25s ease;
}
.eu-progress-text {
color: ${CONFIG.accentSoft};
font-size: 12px;
text-align: center;
font-weight: 600;
}
/* ---------- Floating Action Button (FAB) ---------- */
.eu-fab {
position: fixed;
bottom: 24px; right: 24px;
width: 56px; height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, ${CONFIG.accent}, #6b3fb0);
border: none;
cursor: pointer;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 6px 18px rgba(138,90,204,.55);
transition: all .3s ease;
}
.eu-fab:hover { transform: scale(1.08); box-shadow: 0 8px 26px rgba(138,90,204,.75); }
.eu-fab svg { width: 24px; height: 24px; fill: #fff; }
.eu-fab-badge {
position: absolute;
top: -4px; right: -4px;
min-width: 22px; height: 22px;
padding: 0 6px;
background: ${CONFIG.danger};
color: #fff;
border-radius: 11px;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid ${CONFIG.dark};
}
/* ---------- Hidden Slider (bottom-left) ---------- */
.eu-hidden-slider {
position: fixed;
bottom: 24px; left: 24px;
width: 44px; height: 44px;
border-radius: 50%;
background: ${CONFIG.dark};
border: none;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 3px rgba(138,90,204,.35);
cursor: pointer;
transition: all .3s ease;
z-index: 9999;
color: #fff;
font-weight: 700;
font-size: 12px;
overflow: hidden;
}
.eu-hidden-slider.visible { display: flex; }
.eu-hidden-slider:hover {
width: 110px;
border-radius: 22px;
background: ${CONFIG.accent};
}
.eu-hidden-slider svg { width: 20px; height: 20px; fill: #fff; }
/* ---------- Video Player Custom Controls ---------- */
.video-js .vjs-play-progress {
background: ${CONFIG.accent} !important;
box-shadow: 0 0 18px 4px rgba(138,90,204,.5);
}
.vjs-control:hover { background: rgba(138,90,204,.3); }
.eu-player-btn {
background: ${CONFIG.accent};
color: #fff;
border: none;
border-radius: 5px;
padding: 6px 10px;
margin: 0 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background .2s ease;
font-weight: 600;
font-size: 12px;
}
.eu-player-btn:hover { background: #a77fd4; }
.eu-player-btn svg { width: 16px; height: 16px; fill: #fff; }
/* ---------- Cinema Overlay ---------- */
.eu-cinema-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.92);
z-index: 998;
pointer-events: none;
display: none;
}
body.eu-cinema .eu-cinema-overlay { display: block; }
body.eu-cinema .video-js,
body.eu-cinema .media-group img { z-index: 9999; position: relative; }
/* ---------- Utility ---------- */
.eu-hidden-video { display: none !important; }
.eu-spinner {
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: eu-spin .8s linear infinite;
display: inline-block;
}
`);
/* ============================================================
* ICONS (SVG)
* ============================================================ */
const ICONS = {
download: '<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>',
upload: '<svg viewBox="0 0 384 512"><path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/></svg>',
settings: '<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>',
eye: '<svg viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>',
eyeOff: '<svg viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.82l2.92 2.92c1.51-1.39 2.59-3.26 3.1-5.34-1.71-4.38-5.98-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1.14 12c1.71 4.38 5.98 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27z"/></svg>',
photo: '<svg viewBox="0 0 24 24"><path d="M22 16V4c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2zm-11-4l2.03 2.71L16 11l4 5H8l3-4zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6H2z"/></svg>',
video: '<svg viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>',
cinema: '<svg viewBox="0 0 24 24"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4h-4z"/></svg>',
heart: '<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>',
copy: '<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>',
zip: '<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-4 17h-2v-2h2v2zm0-4h-2v-2h2v2zm0-4h-2V9h2v2zm0-4h-2V5h2v2zm3 12h-2v-2h2v2zm0-4h-2v-2h2v2z"/></svg>',
flip: '<svg viewBox="0 0 16 16"><path d="M7 16V0H9V16H7Z"/><path d="M15 12H14L10 8L14 4H15L15 12Z"/><path d="M2 12H1L1 4H2L6 8L2 12Z"/></svg>',
pip: '<svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>',
clock: '<svg viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm4.2 14.2L11 13V7h1.5v5.2l4.5 2.7-.8 1.3z"/></svg>',
sort: '<svg viewBox="0 0 24 24"><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>',
bell: '<svg viewBox="0 0 24 24"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>',
close: '<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
check: '<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',
star: '<svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>',
starFill: '<svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>',
share: '<svg viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg>',
flag: '<svg viewBox="0 0 24 24"><path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>',
comment: '<svg viewBox="0 0 24 24"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18zM18 14H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>',
user: '<svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>',
music: '<svg viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>'
};
/* ============================================================
* TOAST SYSTEM
* ============================================================ */
const toastWrap = document.createElement('div');
toastWrap.className = 'eu-toast-wrap';
(document.body || document.documentElement).appendChild(toastWrap);
function toast(message, type = 'info', duration = 2600) {
const t = document.createElement('div');
t.className = `eu-toast ${type}`;
const iconMap = { success: ICONS.check, error: ICONS.close, warning: ICONS.bell, info: ICONS.bell };
t.innerHTML = `${iconMap[type] || ICONS.bell}<span>${message}</span>`;
toastWrap.appendChild(t);
setTimeout(() => {
t.style.opacity = '0';
t.style.transform = 'translateX(100px)';
t.style.transition = 'all .3s ease';
setTimeout(() => t.remove(), 350);
}, duration);
}
/* ============================================================
* UTILITIES
* ============================================================ */
function formatFileSize(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
}
function formatDuration(ms) {
let s = Math.floor(ms / 1000);
const h = Math.floor(s / 3600); s %= 3600;
const m = Math.floor(s / 60); s %= 60;
return h > 0
? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
: `${m}:${String(s).padStart(2, '0')}`;
}
function getFilename(url) {
try { return url.split('?')[0].split('/').pop(); }
catch (e) { return 'file'; }
}
function sanitize(name) {
return (name || '').replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').substring(0, 60);
}
function gmRequest(opts) {
const fn = (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest
: (typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null);
if (!fn) throw new Error('GM.xmlHttpRequest not available');
return fn(opts);
}
/* ============================================================
* DOWNLOAD ENGINE
* ============================================================ */
function downloadBlob(url) {
return new Promise((resolve, reject) => {
gmRequest({
method: 'GET',
url,
responseType: 'blob',
headers: {
'User-Agent': 'Mozilla/5.0',
'Referer': 'https://www.erome.com/',
'Accept': '*/*'
},
timeout: 60000,
onload: r => r.status === 200 ? resolve(r.response) : reject(`HTTP ${r.status}`),
onerror: e => reject(e?.error || 'Network error'),
ontimeout: () => reject('Timeout')
});
});
}
async function downloadSingle(url, filename, btn) {
const name = filename || getFilename(url);
const originalHTML = btn ? btn.innerHTML : null;
if (btn) {
btn.classList.add('downloading');
btn.innerHTML = `<span class="eu-spinner"></span><span class="eu-dl-label">Downloading</span>`;
}
try {
const blob = await downloadBlob(url);
const tmp = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = tmp; a.download = name; a.style.display = 'none';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(tmp), 5000);
toast(`Downloaded: ${name}`, 'success');
} catch (err) {
console.error('[EU] Download failed:', err);
toast(`Download failed: ${err}`, 'error');
} finally {
if (btn) {
btn.classList.remove('downloading');
btn.innerHTML = originalHTML;
}
}
}
/* ============================================================
* INLINE DOWNLOAD BUTTONS ON MEDIA
* ============================================================ */
function buildDownloadButton(url) {
const btn = document.createElement('button');
btn.className = 'eu-dl-btn';
btn.innerHTML = `${ICONS.upload}<span class="eu-dl-label">Download</span>`;
btn.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
downloadSingle(url, getFilename(url), btn);
});
return btn;
}
function attachDownloadButtons() {
if (!STATE.showDownloadButtons) return;
document.querySelectorAll('.media-group video, .media-group img').forEach(media => {
const parent = media.parentElement?.parentElement;
if (!parent || parent.querySelector('.eu-dl-btn')) return;
let src;
if (media.tagName === 'IMG') {
src = media.src || media.getAttribute('data-src');
} else {
src = media.querySelector('source')?.src || media.src || media.currentSrc;
}
if (!src) return;
parent.style.position = parent.style.position || 'relative';
parent.appendChild(buildDownloadButton(src));
});
// Also handle album thumbnails on listing pages for image detection
document.querySelectorAll('.album-image img').forEach(img => {
const src = img.src || img.getAttribute('data-src');
if (!src) return;
const host = img.closest('.album-image');
if (!host || host.querySelector('.eu-dl-btn')) return;
host.style.position = host.style.position || 'relative';
host.appendChild(buildDownloadButton(src));
});
}
/* ============================================================
* VIDEO DURATION DETECTION (from file header)
* ============================================================ */
function detectVideoDuration(url) {
return new Promise(resolve => {
if (!url) return resolve(0);
if (STATE.videoDurations.has(url)) return resolve(STATE.videoDurations.get(url));
const cacheKey = 'eu_vl_' + url;
const cached = localStorage.getItem(cacheKey);
if (cached && !isNaN(parseInt(cached))) {
const d = parseInt(cached);
STATE.videoDurations.set(url, d);
return resolve(d);
}
try {
let req = gmRequest({
method: 'GET',
url,
headers: {
'Accept': 'video/webm,video/ogg,video/*;q=0.9,*/*;q=0.5',
'Referer': 'https://www.erome.com/',
'Range': 'bytes=0-500'
},
responseType: 'blob',
onprogress: r => { if (Math.max(r.loaded || 0, r.total || 0) > 600) req.abort?.(); },
onload: r => {
try {
const text = r.responseText || '';
const m = text.match(/\x03.*\xe8/);
if (m) {
const i = text.indexOf(m[0]) + m[0].length;
const s = text.substring(i, i + 4);
const ms = Array.from(s)
.map(c => c.charCodeAt(0))
.map((v, idx, arr) => v * Math.pow(256, arr.length - idx - 1))
.reduce((a, b) => a + b, 0);
localStorage.setItem(cacheKey, ms);
STATE.videoDurations.set(url, ms);
return resolve(ms);
}
} catch (e) {}
resolve(0);
},
onerror: () => resolve(0),
ontimeout: () => resolve(0)
});
} catch (e) { resolve(0); }
});
}
function decorateVideoDurations() {
document.querySelectorAll('.media-group video, .video-js video').forEach(video => {
const container = video.closest('.media-group') || video.closest('.video') || video.parentElement;
if (!container || container.querySelector('.eu-duration-badge')) return;
const src = video.querySelector('source')?.src || video.src || video.currentSrc;
if (!src) return;
detectVideoDuration(src).then(ms => {
if (ms > 0 && !container.querySelector('.eu-duration-badge')) {
const badge = document.createElement('span');
badge.className = 'eu-duration-badge';
badge.textContent = formatDuration(ms);
container.style.position = container.style.position || 'relative';
container.appendChild(badge);
video.dataset.length = Math.floor(ms / 1000);
applyHiddenFilter();
}
});
});
}
/* ============================================================
* ALBUM COUNT BADGES (on listing page)
* ============================================================ */
function decorateAlbumCounts() {
document.querySelectorAll('.album').forEach(album => {
if (album.querySelector('.eu-count-badge')) return;
const imgCount = parseInt(album.querySelector('.album-images')?.textContent?.match(/\d+/)?.[0] || '0');
const vidCount = parseInt(album.querySelector('.album-videos')?.textContent?.match(/\d+/)?.[0] || '0');
if (imgCount === 0 && vidCount === 0) return;
const badge = document.createElement('div');
badge.className = 'eu-count-badge';
const parts = [];
if (imgCount > 0) parts.push(`<span>🖼️ ${imgCount}</span>`);
if (vidCount > 0) parts.push(`<span>🎥 ${vidCount}</span>`);
badge.innerHTML = parts.join('');
const thumb = album.querySelector('.album-thumbnail-container');
if (thumb) {
thumb.style.position = thumb.style.position || 'relative';
thumb.appendChild(badge);
}
});
}
/* ============================================================
* LIKE COUNTS ON ALBUMS (listing page)
* ============================================================ */
async function loadAlbumLikes() {
if (IS_ALBUM_PAGE) return;
const albums = Array.from(document.querySelectorAll('.album-link')).slice(0, 30); // limit to prevent lag
for (const a of albums) {
if (a.dataset.euLiked) continue;
a.dataset.euLiked = '1';
try {
const res = await fetch(a.href);
const text = await res.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
const likeArea = doc.querySelector('#like_count') || doc.querySelector('.far.fa-heart.fa-lg');
const count = likeArea?.nextElementSibling?.firstChild?.textContent?.trim() || 0;
if (+count < 1) continue;
const viewSec = a.parentElement.querySelector('.album-bottom-right .album-images');
if (!viewSec) continue;
viewSec.insertAdjacentHTML('afterbegin',
`<span class="eu-likes-badge">${ICONS.heart} ${count}</span>`);
} catch (e) { /* silent */ }
}
}
/* ============================================================
* SORT ALBUMS BY VIEWS
* ============================================================ */
function parseViews(text) {
if (!text) return 0;
const hasK = /k/i.test(text);
const num = parseFloat(text.replace(/[^\d.,]/g, '').replace(',', '.'));
return (isNaN(num) ? 0 : num) * (hasK ? 1000 : 1);
}
function parseCount(text) {
if (!text) return 0;
const n = parseInt(text.replace(/[^\d]/g, ''));
return isNaN(n) ? 0 : n;
}
function getAlbumMetric(album, mode) {
switch (mode) {
case 'videos': return parseCount(album.querySelector('.album-videos')?.textContent);
case 'photos': return parseCount(album.querySelector('.album-images')?.textContent);
case 'views':
default: return parseViews(album.querySelector('.album-bottom-views')?.textContent);
}
}
function sortAlbums(mode) {
if (mode) STATE.sortMode = mode;
const container = document.querySelector('#albums') || document.querySelector('.page-content');
if (!container) return;
const albums = Array.from(container.querySelectorAll('.album'));
if (albums.length < 2) return;
albums.sort((a, b) => {
const va = getAlbumMetric(a, STATE.sortMode);
const vb = getAlbumMetric(b, STATE.sortMode);
return STATE.sortAscending ? va - vb : vb - va;
});
albums.forEach(a => container.appendChild(a));
const labels = { views: 'views', videos: 'video count', photos: 'photo count' };
toast(`Sorted by ${labels[STATE.sortMode]} (${STATE.sortAscending ? 'ascending' : 'descending'})`, 'info');
}
// Backward-compatible alias
function sortAlbumsByViews() { sortAlbums('views'); }
/* ============================================================
* TOGGLES: photos / videos / downloads / NSFW / cinema / video-only
* ============================================================ */
function togglePhotos() {
STATE.showPhotos = !STATE.showPhotos;
document.querySelectorAll('.media-group img').forEach(el => {
el.style.display = STATE.showPhotos ? '' : 'none';
});
toast(STATE.showPhotos ? 'Photos visible' : 'Photos hidden', 'info');
}
function toggleVideos() {
STATE.showVideos = !STATE.showVideos;
document.querySelectorAll('.video-js, .media-group video').forEach(el => {
el.style.display = STATE.showVideos ? '' : 'none';
});
toast(STATE.showVideos ? 'Videos visible' : 'Videos hidden', 'info');
}
function toggleDownloadButtons() {
STATE.showDownloadButtons = !STATE.showDownloadButtons;
document.querySelectorAll('.eu-dl-btn').forEach(btn => {
btn.style.display = STATE.showDownloadButtons ? '' : 'none';
});
toast(STATE.showDownloadButtons ? 'Download buttons visible' : 'Download buttons hidden', 'info');
}
function toggleNSFW() {
STATE.nsfwBlur = !STATE.nsfwBlur;
saveBool('eu_nsfw', STATE.nsfwBlur);
applyNSFW();
toast(STATE.nsfwBlur ? 'NSFW blur enabled' : 'NSFW blur disabled', 'info');
}
function applyNSFW() {
const targets = document.querySelectorAll(
'.album-thumbnail-container, .media-group img, .media-group video, .vjs-poster'
);
targets.forEach(el => el.classList.toggle('eu-blur', STATE.nsfwBlur));
}
function toggleCinema() {
STATE.cinemaMode = !STATE.cinemaMode;
document.body.classList.toggle('eu-cinema', STATE.cinemaMode);
if (STATE.cinemaMode && !document.querySelector('.eu-cinema-overlay')) {
const o = document.createElement('div');
o.className = 'eu-cinema-overlay';
document.body.appendChild(o);
}
toast(STATE.cinemaMode ? 'Cinema mode on' : 'Cinema mode off', 'info');
}
function toggleVideoOnly() {
STATE.videoOnlyMode = !STATE.videoOnlyMode;
saveBool('eu_video_only', STATE.videoOnlyMode);
applyVideoOnly();
toast(STATE.videoOnlyMode ? 'Showing video albums only' : 'Showing all albums', 'info');
}
function applyVideoOnly() {
document.querySelectorAll('.album').forEach(a => {
const hasVideo = !!a.querySelector('.album-videos');
a.style.display = (STATE.videoOnlyMode && !hasVideo) ? 'none' : '';
});
}
/* ============================================================
* HIDDEN BY DURATION (bottom-left slider)
* ============================================================ */
function applyHiddenFilter() {
document.querySelectorAll('.video').forEach(vc => {
const video = vc.querySelector('.video-js video');
if (!video || !video.dataset.length) { vc.classList.remove('eu-hidden-video'); return; }
const len = parseInt(video.dataset.length);
if (STATE.hiddenSeconds > 0 && len < STATE.hiddenSeconds) {
vc.classList.add('eu-hidden-video');
} else {
vc.classList.remove('eu-hidden-video');
}
});
}
function createHiddenSlider() {
if (!IS_ALBUM_PAGE) return;
if (document.querySelector('.eu-hidden-slider')) return;
const btn = document.createElement('button');
btn.className = 'eu-hidden-slider';
btn.dataset.count = '0';
btn.innerHTML = ICONS.clock;
btn.title = 'Hide videos shorter than (seconds)';
btn.addEventListener('mouseenter', () => {
const n = parseInt(btn.dataset.count);
btn.innerHTML = n > 0 ? `${n}s` : 'OFF';
});
btn.addEventListener('mouseleave', () => { btn.innerHTML = ICONS.clock; });
btn.addEventListener('click', () => {
let n = parseInt(btn.dataset.count);
n = (n + 5) % 105;
if (n >= 100) n = 0;
btn.dataset.count = n;
STATE.hiddenSeconds = n;
btn.innerHTML = n > 0 ? `${n}s` : 'OFF';
applyHiddenFilter();
toast(n > 0 ? `Hiding videos shorter than ${n}s` : 'Duration filter off', 'info');
});
document.body.appendChild(btn);
}
/* ============================================================
* VIDEO.JS PLAYER CUSTOM CONTROLS
* ============================================================ */
const SPEEDS = [0.5, 1, 1.5, 2, 3, 5];
function enhancePlayers() {
if (typeof window.videojs === 'undefined') {
// Wait for video.js to load
setTimeout(enhancePlayers, 500);
return;
}
document.querySelectorAll('.video-js').forEach(el => {
if (el.dataset.euEnhanced) return;
el.dataset.euEnhanced = '1';
let player;
try { player = window.videojs(el); }
catch (e) { return; }
player.ready(() => {
const bar = player.controlBar?.el?.();
if (!bar) return;
// Speed
let speedIdx = 1;
const speedBtn = document.createElement('button');
speedBtn.className = 'eu-player-btn';
speedBtn.textContent = '1x';
speedBtn.title = 'Playback speed';
speedBtn.onclick = () => {
speedIdx = (speedIdx + 1) % SPEEDS.length;
player.playbackRate(SPEEDS[speedIdx]);
speedBtn.textContent = SPEEDS[speedIdx] + 'x';
toast(`Speed: ${SPEEDS[speedIdx]}x`, 'info', 1500);
};
// Download
const dlBtn = document.createElement('button');
dlBtn.className = 'eu-player-btn';
dlBtn.innerHTML = ICONS.download;
dlBtn.title = 'Download video';
dlBtn.onclick = () => {
const src = player.currentSrc();
if (src) downloadSingle(src, getFilename(src));
};
// Flip
let flipped = false;
const flipBtn = document.createElement('button');
flipBtn.className = 'eu-player-btn';
flipBtn.innerHTML = ICONS.flip;
flipBtn.title = 'Flip video';
flipBtn.onclick = () => {
const v = el.querySelector('video');
flipped = !flipped;
if (v) v.style.transform = flipped ? 'scaleX(-1)' : '';
toast('Video flipped', 'info', 1500);
};
// Cinema
const cineBtn = document.createElement('button');
cineBtn.className = 'eu-player-btn';
cineBtn.innerHTML = ICONS.cinema;
cineBtn.title = 'Cinema mode';
cineBtn.onclick = toggleCinema;
// PiP
const pipBtn = document.createElement('button');
pipBtn.className = 'eu-player-btn';
pipBtn.innerHTML = ICONS.pip;
pipBtn.title = 'Picture-in-Picture';
pipBtn.onclick = async () => {
try {
const v = el.querySelector('video');
if (document.pictureInPictureElement) { await document.exitPictureInPicture(); toast('PiP off', 'info', 1500); }
else if (v) { await v.requestPictureInPicture(); toast('PiP on', 'info', 1500); }
} catch (e) { toast('PiP not supported', 'error'); }
};
bar.appendChild(speedBtn);
bar.appendChild(dlBtn);
bar.appendChild(flipBtn);
bar.appendChild(cineBtn);
bar.appendChild(pipBtn);
});
});
}
/* ============================================================
* BULK DOWNLOAD MODAL
* ============================================================ */
function collectAllMediaUrls() {
const urls = new Set();
document.querySelectorAll('.media-group img, img.media, .album-image img').forEach(img => {
const src = img.src || img.getAttribute('data-src') || img.getAttribute('data-original');
if (src && !src.includes('thumb') && !src.includes('logo') && !src.includes('avatar')) {
try { urls.add(new URL(src, location.href).href); } catch (e) {}
}
});
document.querySelectorAll('.media-group video, .video-js video, video, video source').forEach(v => {
const src = v.src || v.getAttribute('src') || v.currentSrc;
if (src) { try { urls.add(new URL(src, location.href).href); } catch (e) {} }
});
return Array.from(urls);
}
function isImageUrl(u) { return /\.(jpe?g|png|gif|webp|bmp)(\?|$)/i.test(u); }
function isVideoUrl(u) { return /\.(mp4|webm|mov|avi|m3u8)(\?|$)/i.test(u); }
function getPageTitle() {
const el = document.querySelector('h1, .album-title');
let title = el?.textContent?.trim() || document.title || 'Erome_Album';
return sanitize(title) || 'Erome_Album';
}
function generateZipName() {
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').substring(0, 16);
return `Erome_${stamp}_${getPageTitle()}.zip`;
}
function buildBulkModal() {
if (document.getElementById('eu-bulk-modal')) return;
const overlay = document.createElement('div');
overlay.id = 'eu-bulk-modal';
overlay.className = 'eu-modal-overlay';
overlay.innerHTML = `
<div class="eu-modal">
<div class="eu-modal-header">
<h3>${ICONS.download}<span>Bulk Download</span></h3>
<button class="eu-modal-close">${ICONS.close}</button>
</div>
<div class="eu-modal-body">
<div class="eu-option-group">
<div class="eu-option-title">Media Type</div>
<label class="eu-option"><input type="radio" name="eu-type" value="all" checked><span class="eu-radio"></span>All files</label>
<label class="eu-option"><input type="radio" name="eu-type" value="images"><span class="eu-radio"></span>Images only</label>
<label class="eu-option"><input type="radio" name="eu-type" value="videos"><span class="eu-radio"></span>Videos only</label>
</div>
<div class="eu-option-group">
<div class="eu-option-title">Output</div>
<label class="eu-option"><input type="checkbox" id="eu-zip" checked><span class="eu-check"></span>Package as ZIP</label>
<div class="eu-zip-preview" id="eu-zip-name"></div>
</div>
<div class="eu-option-group">
<div class="eu-option-title">Stats</div>
<div class="eu-zip-preview" id="eu-stats">Scanning…</div>
</div>
<button class="eu-btn-primary" id="eu-start-dl">${ICONS.download}<span>Start Download</span></button>
<div class="eu-progress-box">
<div class="eu-progress-bar"><div class="eu-progress-fill" id="eu-progress-fill"></div></div>
<div class="eu-progress-text" id="eu-progress-text">Ready</div>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.querySelector('.eu-modal-close').onclick = () => overlay.classList.remove('open');
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.classList.remove('open'); });
const refreshPreview = () => {
document.getElementById('eu-zip-name').textContent = generateZipName();
const urls = collectAllMediaUrls();
const imgs = urls.filter(isImageUrl).length;
const vids = urls.filter(isVideoUrl).length;
document.getElementById('eu-stats').textContent = `Found ${urls.length} items (${imgs} images, ${vids} videos)`;
};
overlay.querySelectorAll('input[name="eu-type"]').forEach(r => r.addEventListener('change', refreshPreview));
document.getElementById('eu-zip').addEventListener('change', refreshPreview);
document.getElementById('eu-start-dl').onclick = () => bulkDownload(overlay);
// Refresh preview whenever opened
overlay.dataset.refresh = '1';
overlay.addEventListener('transitionstart', refreshPreview);
overlay._refreshPreview = refreshPreview;
}
function openBulkModal() {
buildBulkModal();
const m = document.getElementById('eu-bulk-modal');
m.classList.add('open');
m._refreshPreview?.();
}
async function bulkDownload(overlay) {
if (STATE.downloadInProgress) { toast('A download is already running', 'warning'); return; }
const type = overlay.querySelector('input[name="eu-type"]:checked').value;
const asZip = overlay.querySelector('#eu-zip').checked;
const btn = overlay.querySelector('#eu-start-dl');
const fill = overlay.querySelector('#eu-progress-fill');
const text = overlay.querySelector('#eu-progress-text');
let urls = collectAllMediaUrls();
if (type === 'images') urls = urls.filter(isImageUrl);
if (type === 'videos') urls = urls.filter(isVideoUrl);
if (urls.length === 0) { toast('No media found', 'error'); return; }
STATE.downloadInProgress = true;
btn.disabled = true;
btn.innerHTML = `<span class="eu-spinner"></span><span>Working…</span>`;
fill.style.width = '0%';
text.textContent = `0 / ${urls.length}`;
try {
if (asZip && typeof JSZip !== 'undefined') {
const zip = new JSZip();
let ok = 0;
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
const name = `${String(i + 1).padStart(3, '0')}_${getFilename(url)}`;
text.textContent = `Downloading ${i + 1}/${urls.length}: ${name.substring(0, 30)}`;
fill.style.width = `${(i / urls.length) * 100}%`;
try {
const blob = await downloadBlob(url);
zip.file(name, await blob.arrayBuffer());
ok++;
} catch (e) {
zip.file(`ERROR_${i + 1}.txt`, `URL: ${url}\nError: ${e}`);
}
}
text.textContent = 'Generating ZIP…';
const zipBlob = await zip.generateAsync(
{ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } },
meta => { text.textContent = `Compressing: ${Math.round(meta.percent)}%`; fill.style.width = `${meta.percent}%`; }
);
if (typeof saveAs === 'function') saveAs(zipBlob, generateZipName());
else {
const a = document.createElement('a');
a.href = URL.createObjectURL(zipBlob);
a.download = generateZipName();
a.click();
}
fill.style.width = '100%';
text.textContent = `Complete (${ok}/${urls.length})`;
toast(`ZIP saved: ${ok}/${urls.length} files`, 'success');
} else {
let ok = 0;
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
const name = `${String(i + 1).padStart(3, '0')}_${getFilename(url)}`;
text.textContent = `Downloading ${i + 1}/${urls.length}`;
fill.style.width = `${(i / urls.length) * 100}%`;
try {
const blob = await downloadBlob(url);
const u = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = u; a.download = name; a.style.display = 'none';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(u), 5000);
ok++;
await new Promise(r => setTimeout(r, 400));
} catch (e) { console.error(e); }
}
fill.style.width = '100%';
text.textContent = `Complete (${ok}/${urls.length})`;
toast(`Downloaded ${ok}/${urls.length} files`, 'success');
}
} catch (err) {
console.error(err);
toast('Bulk download error', 'error');
} finally {
STATE.downloadInProgress = false;
btn.disabled = false;
btn.innerHTML = `${ICONS.download}<span>Start Download</span>`;
}
}
/* ============================================================
* TIKTOK-STYLE VERTICAL SCROLL MODE — Phone-proportioned
* ============================================================ */
GM_addStyle(`
.eu-tiktok-overlay {
position: fixed; inset: 0;
background: radial-gradient(circle at 50% 30%, #1a1a2e 0%, #000 70%);
z-index: 99999;
display: none;
align-items: center;
justify-content: center;
animation: eu-fade-in .3s ease;
}
.eu-tiktok-overlay.open { display: flex; }
/* Phone-shaped container (9:16 aspect ratio, like a real phone) */
.eu-phone {
position: relative;
width: min(420px, 92vw);
height: min(calc(420px * 16 / 9), 94vh);
aspect-ratio: 9 / 16;
background: #000;
border-radius: 28px;
overflow: hidden;
box-shadow:
0 0 0 8px #111,
0 0 0 9px #2a2a2a,
0 30px 80px rgba(138, 90, 204, .25),
0 60px 120px rgba(0, 0, 0, .7);
display: flex;
flex-direction: column;
}
/* Phone notch */
.eu-phone::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 110px;
height: 22px;
background: #0a0a0a;
border-radius: 14px;
z-index: 100;
}
@media (max-width: 520px) {
.eu-phone {
width: 100vw;
height: 100vh;
border-radius: 0;
box-shadow: none;
aspect-ratio: auto;
}
.eu-phone::before { display: none; }
}
.eu-tiktok-topbar {
position: absolute;
top: 0; left: 0; right: 0;
padding-top: 36px;
height: 86px;
background: linear-gradient(180deg, rgba(0,0,0,.9), transparent);
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-left: 14px;
padding-right: 14px;
z-index: 20;
pointer-events: none;
}
.eu-tiktok-topbar > * { pointer-events: auto; }
.eu-tiktok-title {
color: #fff;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.eu-tiktok-title span {
border-bottom: 2px solid ${CONFIG.accent};
padding-bottom: 2px;
}
.eu-tiktok-title svg { width: 16px; height: 16px; fill: #fff; }
.eu-tiktok-brand-logo {
height: 22px; width: auto;
filter: drop-shadow(0 0 6px rgba(138,90,204,0.7));
border-radius: 4px;
}
/* === Splash screen (shown when TikTok mode opens) === */
.eu-tiktok-splash {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: linear-gradient(135deg, #0e0f17 0%, #14151f 50%, #2a1f3d 100%);
z-index: 100;
gap: 18px;
animation: eu-splash-fade 1.8s ease forwards;
animation-delay: 0.8s;
pointer-events: none;
}
.eu-tiktok-splash-logo {
max-width: 76%; width: 360px; height: auto;
filter: drop-shadow(0 0 40px rgba(138,90,204,0.7));
animation: eu-splash-pop .7s cubic-bezier(.2,.9,.3,1.2);
}
.eu-tiktok-splash-tag {
color: #b39ad6; font-size: 13px; font-weight: 700;
letter-spacing: 4px; text-transform: uppercase;
animation: eu-splash-pop .9s cubic-bezier(.2,.9,.3,1.2);
}
.eu-tiktok-splash-dots {
display: flex; gap: 8px;
}
.eu-tiktok-splash-dots span {
width: 8px; height: 8px; border-radius: 50%;
background: #8a5acc;
animation: eu-splash-dot 1.2s ease-in-out infinite;
}
.eu-tiktok-splash-dots span:nth-child(2) { animation-delay: .2s; }
.eu-tiktok-splash-dots span:nth-child(3) { animation-delay: .4s; }
@keyframes eu-splash-pop {
0% { opacity: 0; transform: scale(.6); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes eu-splash-dot {
0%, 100% { opacity: 0.3; transform: translateY(0); }
50% { opacity: 1; transform: translateY(-6px); }
}
@keyframes eu-splash-fade {
0% { opacity: 1; }
85% { opacity: 1; }
100% { opacity: 0; visibility: hidden; pointer-events: none; }
}
.eu-tiktok-controls {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
.eu-tiktok-tab {
background: rgba(255,255,255,.12);
color: #fff;
border: none;
padding: 5px 10px;
border-radius: 14px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
backdrop-filter: blur(6px);
}
.eu-tiktok-tab:hover { background: rgba(255,255,255,.22); }
.eu-tiktok-tab.active {
background: ${CONFIG.accent};
box-shadow: 0 0 12px rgba(138,90,204,.6);
}
.eu-tiktok-close {
background: rgba(0,0,0,.65);
color: #fff;
border: none;
width: 32px; height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background .2s ease;
backdrop-filter: blur(6px);
}
.eu-tiktok-close:hover { background: rgba(255,255,255,.2); }
.eu-tiktok-close svg { width: 16px; height: 16px; fill: #fff; }
.eu-tiktok-feed {
flex: 1;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
-ms-overflow-style: none;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.eu-tiktok-feed::-webkit-scrollbar { display: none; }
.eu-tiktok-item {
position: relative;
width: 100%;
height: 100%;
min-height: 100%;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
align-items: center;
justify-content: center;
background: #000;
overflow: hidden;
}
.eu-tiktok-media {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.eu-tiktok-item::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
box-shadow: inset 0 -140px 160px -40px rgba(0,0,0,.55), inset 0 70px 80px -40px rgba(0,0,0,.35);
}
/* Right sidebar — TikTok style */
.eu-tiktok-sidebar {
position: absolute;
right: 10px;
bottom: 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
z-index: 5;
max-height: calc(100% - 48px);
}
.eu-tiktok-user-link {
color: #fff !important;
text-decoration: none;
}
.eu-tiktok-action {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
color: #fff;
cursor: pointer;
transition: transform .15s ease;
}
.eu-tiktok-action:hover { transform: scale(1.12); }
.eu-tiktok-action:active { transform: scale(.92); }
.eu-tiktok-action .eu-tiktok-icon {
width: 40px; height: 40px;
border-radius: 50%;
background: rgba(0,0,0,.55);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255,255,255,.12);
transition: all .2s ease;
}
.eu-tiktok-action .eu-tiktok-icon svg { width: 20px; height: 20px; fill: #fff; }
.eu-tiktok-action.liked .eu-tiktok-icon { background: #ef4444; animation: eu-pulse .35s ease; }
.eu-tiktok-action .eu-tiktok-lbl {
font-size: 10px;
font-weight: 700;
text-shadow: 0 0 6px rgba(0,0,0,.95);
}
/* Bottom caption */
.eu-tiktok-caption {
position: absolute;
left: 14px;
right: 80px;
bottom: 22px;
color: #fff;
z-index: 4;
padding-bottom: 12px;
background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 100%);
padding-top: 40px; padding-left: 0; padding-right: 0;
margin-left: -14px; margin-right: -80px;
padding-left: 14px; padding-right: 80px;
}
.eu-tiktok-caption .eu-tiktok-user {
font-weight: 700;
font-size: 14px;
margin-bottom: 4px;
text-shadow: 0 0 8px rgba(0,0,0,.95);
}
.eu-tiktok-caption .eu-tiktok-desc {
font-size: 12px;
line-height: 1.35;
opacity: .92;
text-shadow: 0 0 8px rgba(0,0,0,.95);
max-height: 54px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.eu-tiktok-caption .eu-tiktok-album {
display: inline-block;
margin-top: 6px;
font-size: 11px;
padding: 3px 8px;
background: rgba(138,90,204,.4);
border-radius: 8px;
backdrop-filter: blur(6px);
color: #fff;
}
.eu-tiktok-counter {
position: absolute;
top: 95px;
left: 14px;
background: rgba(0,0,0,.55);
color: #fff;
padding: 4px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 700;
z-index: 5;
backdrop-filter: blur(6px);
}
.eu-tiktok-play-hint {
position: absolute;
width: 64px; height: 64px;
background: rgba(0,0,0,.65);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
opacity: 0;
transition: opacity .2s ease;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
.eu-tiktok-play-hint.show { opacity: 1; }
.eu-tiktok-play-hint svg { width: 32px; height: 32px; fill: #fff; }
/* Video progress bar (at top of item) */
.eu-tiktok-progress {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 3px;
background: rgba(255,255,255,.12);
z-index: 6;
}
.eu-tiktok-progress-fill {
height: 100%;
background: #fff;
width: 0;
transition: width .1s linear;
}
/* === Erome-native buttons === */
.eu-tiktok-top {
position: absolute; top: 14px; left: 14px; right: 70px;
display: flex; align-items: center; gap: 8px;
z-index: 10; pointer-events: none;
}
.eu-tiktok-views, .eu-tiktok-kind-pill {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(0,0,0,0.55); backdrop-filter: blur(8px);
color: #fff; font-size: 11px; font-weight: 600;
padding: 5px 10px; border-radius: 20px;
border: 1px solid rgba(255,255,255,0.1);
}
.eu-tiktok-views svg, .eu-tiktok-kind-pill svg { width: 12px; height: 12px; fill: #fff; }
/* Avatar button (profile + follow plus sign) */
.eu-tiktok-avatar {
position: relative; width: 52px; height: 52px;
border-radius: 50%; overflow: visible;
border: 2px solid #fff; margin-bottom: 6px;
display: block; flex-shrink: 0;
transition: transform .2s ease;
}
.eu-tiktok-avatar:hover { transform: scale(1.08); }
.eu-tiktok-avatar img {
width: 100%; height: 100%; border-radius: 50%;
object-fit: cover; background: #8a5acc;
display: block;
}
.eu-tiktok-avatar-plus {
position: absolute; bottom: -7px; left: 50%;
transform: translateX(-50%);
width: 20px; height: 20px; border-radius: 50%;
background: #ff0050; color: #fff;
font-size: 14px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
line-height: 1; border: 2px solid transparent;
}
/* Action button tweaks for Erome-native look */
.eu-tiktok-action.faved .eu-tiktok-icon { background: #f59e0b; animation: eu-pulse .35s ease; }
.eu-tiktok-action.faved .eu-tiktok-icon svg { fill: #fff; }
.eu-tiktok-action-mini .eu-tiktok-icon {
width: 36px !important; height: 36px !important;
background: rgba(0,0,0,.55) !important;
}
.eu-tiktok-action-mini .eu-tiktok-icon svg { width: 16px !important; height: 16px !important; }
.eu-tiktok-count {
font-variant-numeric: tabular-nums;
min-width: 20px; text-align: center;
}
/* Spinning music disc (TikTok-style) */
.eu-tiktok-disc {
width: 48px; height: 48px; border-radius: 50%;
overflow: hidden; border: 6px solid #111;
background: #000; flex-shrink: 0;
animation: eu-spin 6s linear infinite;
box-shadow: 0 2px 10px rgba(0,0,0,0.6);
margin-top: 4px;
}
.eu-tiktok-disc img { width: 100%; height: 100%; object-fit: cover; }
@keyframes eu-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* Bottom-left scrolling ticker (music row) */
.eu-tiktok-ticker {
display: flex; align-items: center; gap: 6px;
color: #fff; font-size: 12px; opacity: .85;
margin-top: 6px;
white-space: nowrap; overflow: hidden;
}
.eu-tiktok-ticker svg { width: 13px; height: 13px; fill: #fff; flex-shrink: 0; }
.eu-tiktok-ticker span {
display: inline-block; animation: eu-ticker 12s linear infinite;
padding-left: 0;
}
@keyframes eu-ticker {
0% { transform: translateX(0); }
50% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* "View Album on Erome" button — more prominent */
.eu-tiktok-caption .eu-tiktok-album {
display: inline-flex !important; align-items: center; gap: 6px;
margin-top: 6px;
background: rgba(138, 90, 204, 0.25);
border: 1px solid rgba(138, 90, 204, 0.5);
padding: 6px 12px; border-radius: 20px;
font-size: 12px; font-weight: 600;
text-decoration: none; color: #fff !important;
transition: all .2s ease;
}
.eu-tiktok-caption .eu-tiktok-album:hover {
background: rgba(138, 90, 204, 0.5);
transform: translateY(-1px);
}
.eu-tiktok-caption .eu-tiktok-album svg { width: 12px; height: 12px; fill: #fff; }
/* Loading spinner */
.eu-tiktok-loading {
color: #fff;
text-align: center;
padding: 24px;
font-size: 13px;
opacity: .7;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.eu-tiktok-empty {
color: #aaa;
font-size: 13px;
text-align: center;
padding: 40px 20px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Phone hint */
.eu-tiktok-hint {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,.5);
font-size: 11px;
text-align: center;
z-index: 2;
pointer-events: none;
animation: eu-hint-bounce 2s ease infinite;
}
@keyframes eu-hint-bounce {
0%, 100% { transform: translate(-50%, 0); opacity: .5; }
50% { transform: translate(-50%, -6px); opacity: 1; }
}
/* Double-tap heart animation */
.eu-tiktok-heart-pop {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%) scale(0);
font-size: 120px;
pointer-events: none;
opacity: 0;
z-index: 50;
}
.eu-tiktok-heart-pop.animate {
animation: eu-heart-pop .8s ease;
}
@keyframes eu-heart-pop {
0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
30% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
70% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1.4); opacity: 0; }
}
`);
/* --- TikTok State --- */
const TT = {
items: [], // All collected media items (flattened)
seenUrls: new Set(), // Dedupe set
albumQueue: [], // Album URLs queued for preload
albumSeen: new Set(), // Already preloaded album URLs
loadingMore: false,
io: null, // IntersectionObserver
currentAlbum: null
};
function extractMediaFromDoc(doc, type, albumMeta = {}) {
const results = [];
const add = (kind, url) => {
if (!url || TT.seenUrls.has(url)) return;
if (url.includes('thumb') || url.includes('avatar') || url.includes('logo')) return;
TT.seenUrls.add(url);
results.push({ kind, url, ...albumMeta });
};
if (type === 'videos' || type === 'all') {
doc.querySelectorAll('.media-group video, .video-js video, video').forEach(v => {
const src = v.querySelector('source')?.src || v.src || v.getAttribute('src');
if (src) add('video', src.startsWith('//') ? 'https:' + src : src);
});
doc.querySelectorAll('.media-group source, video source').forEach(s => {
const src = s.src || s.getAttribute('src');
if (src) add('video', src.startsWith('//') ? 'https:' + src : src);
});
}
if (type === 'photos' || type === 'all') {
doc.querySelectorAll('.media-group img, .album-image img, img.img-front').forEach(img => {
const src = img.src || img.getAttribute('data-src') || img.getAttribute('src');
if (src) add('image', src.startsWith('//') ? 'https:' + src : src);
});
}
return results;
}
function getAlbumMetaFromDoc(doc, fallbackUrl) {
const userEl = doc.getElementById('user_name') || doc.querySelector('.username');
const titleEl = doc.querySelector('.page-content h1, h1');
// Try to pull like/favorite/view counts from Erome's native DOM
const grabCount = (selector) => {
const el = doc.querySelector(selector);
if (!el) return '';
const m = (el.textContent || '').match(/[\d,.]+[kKmM]?/);
return m ? m[0] : '';
};
return {
username: userEl?.textContent?.replace(/\s+/g, ' ').trim() || 'Erome',
title: titleEl?.textContent?.trim() || 'Album',
albumUrl: fallbackUrl || location.href,
likeCount: grabCount('#like_count') || grabCount('.likes-count') || grabCount('.fa-heart + span') || '',
favCount: grabCount('#favorites_count') || grabCount('.favorites-count') || grabCount('.fa-star + span') || '',
viewCount: grabCount('#views_count') || grabCount('.views-count') || grabCount('.fa-eye + span') || ''
};
}
function collectCurrentPageItems(type) {
TT.seenUrls = new Set();
const meta = getAlbumMetaFromDoc(document, location.href);
TT.currentAlbum = meta;
return extractMediaFromDoc(document, type, meta);
}
function buildNextAlbumQueue() {
// Collect related album links on the current page (other albums, same user, suggested, etc.)
const links = new Set();
document.querySelectorAll('a.album-link, .album a[href*="/a/"]').forEach(a => {
const href = a.getAttribute('href');
if (href && /\/a\//.test(href) && href !== location.pathname) {
const abs = new URL(href, location.href).href;
if (!TT.albumSeen.has(abs)) links.add(abs);
}
});
TT.albumQueue = Array.from(links);
console.log(`[EU] TikTok queue: ${TT.albumQueue.length} related albums ready to preload`);
}
async function fetchAlbumPage(url) {
try {
const res = await fetch(url, { credentials: 'include' });
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc;
} catch (e) {
console.warn('[EU] Failed to preload album:', url, e);
return null;
}
}
async function preloadNextAlbum() {
if (TT.loadingMore) return 0;
if (TT.albumQueue.length === 0) {
buildNextAlbumQueue();
if (TT.albumQueue.length === 0) return 0;
}
TT.loadingMore = true;
showLoadingCard(true);
const url = TT.albumQueue.shift();
TT.albumSeen.add(url);
const doc = await fetchAlbumPage(url);
let added = 0;
if (doc) {
const meta = getAlbumMetaFromDoc(doc, url);
const newItems = extractMediaFromDoc(doc, STATE.tiktokType, meta);
added = newItems.length;
if (added > 0) {
TT.items.push(...newItems);
newItems.forEach((item, i) => appendTikTokCard(item, TT.items.length - added + i));
updateTikTokCounter();
console.log(`[EU] Preloaded ${added} items from: ${meta.title}`);
}
}
showLoadingCard(false);
TT.loadingMore = false;
// If this album produced nothing useful, try the next one automatically
if (added === 0 && TT.albumQueue.length > 0) {
return preloadNextAlbum();
}
return added;
}
function showLoadingCard(show) {
const feed = document.getElementById('eu-tiktok-feed');
if (!feed) return;
let card = feed.querySelector('.eu-tiktok-loading');
if (show) {
if (!card) {
card = document.createElement('div');
card.className = 'eu-tiktok-loading';
card.innerHTML = `<span class="eu-spinner"></span><span>Loading next album…</span>`;
feed.appendChild(card);
}
} else if (card) {
card.remove();
}
}
function updateTikTokCounter() {
const el = document.getElementById('eu-tiktok-counter');
if (!el) return;
const idx = Math.min(STATE.tiktokIndex + 1, TT.items.length);
el.textContent = `${idx} / ${TT.items.length}`;
}
function appendTikTokCard(item, idx) {
const feed = document.getElementById('eu-tiktok-feed');
if (!feed) return;
const el = document.createElement('div');
el.className = 'eu-tiktok-item';
el.dataset.idx = idx;
el._euData = item;
let mediaHTML;
if (item.kind === 'video') {
mediaHTML = `<video class="eu-tiktok-media" src="${item.url}" loop playsinline preload="metadata" muted></video>`;
} else {
mediaHTML = `<img class="eu-tiktok-media" src="${item.url}" alt="" loading="lazy">`;
}
const descText = (item.title || '').replace(/[<>]/g, '');
const kindEmoji = item.kind === 'video' ? '🎥' : '🖼️';
const username = (item.username || 'Erome').replace(/[<>]/g, '');
const avatarSeed = encodeURIComponent(username);
const likeCount = item.likeCount || '';
const favCount = item.favCount || '';
const viewCount = item.viewCount || '';
const albumHref = item.albumUrl || '#';
el.innerHTML = `
${mediaHTML}
<div class="eu-tiktok-play-hint">${ICONS.video}</div>
<div class="eu-tiktok-heart-pop">❤️</div>
<div class="eu-tiktok-top">
${viewCount ? `<span class="eu-tiktok-views">${ICONS.eye}<span>${viewCount}</span></span>` : ''}
<span class="eu-tiktok-kind-pill">${kindEmoji} ${item.kind === 'video' ? 'Video' : 'Photo'}</span>
</div>
<div class="eu-tiktok-caption">
<a class="eu-tiktok-user" href="${albumHref}" target="_blank" rel="noopener">@${username}</a>
<div class="eu-tiktok-desc">${descText || `${kindEmoji} Media`}</div>
${item.albumUrl ? `<a class="eu-tiktok-album" href="${item.albumUrl}" target="_blank" rel="noopener">${ICONS.eye}<span>View Album on Erome</span></a>` : ''}
<div class="eu-tiktok-ticker">
${ICONS.music}<span>Erome · @${username}</span>
</div>
</div>
<div class="eu-tiktok-sidebar">
<a class="eu-tiktok-avatar" href="${albumHref}" target="_blank" rel="noopener" title="Visit @${username}">
<img src="https://api.dicebear.com/7.x/initials/svg?seed=${avatarSeed}&backgroundColor=8a5acc" alt="${username}">
<span class="eu-tiktok-avatar-plus">+</span>
</a>
<div class="eu-tiktok-action" data-act="like" title="Like">
<div class="eu-tiktok-icon">${ICONS.heart}</div>
<div class="eu-tiktok-lbl eu-tiktok-count">${likeCount || 'Like'}</div>
</div>
<div class="eu-tiktok-action" data-act="favorite" title="Add to Favorites">
<div class="eu-tiktok-icon">${ICONS.star}</div>
<div class="eu-tiktok-lbl eu-tiktok-count">${favCount || 'Favorite'}</div>
</div>
<div class="eu-tiktok-action" data-act="comment" title="Comments">
<div class="eu-tiktok-icon">${ICONS.comment}</div>
<div class="eu-tiktok-lbl">Comments</div>
</div>
<div class="eu-tiktok-action" data-act="share" title="Share">
<div class="eu-tiktok-icon">${ICONS.share}</div>
<div class="eu-tiktok-lbl">Share</div>
</div>
<div class="eu-tiktok-action" data-act="download" title="Save">
<div class="eu-tiktok-icon">${ICONS.download}</div>
<div class="eu-tiktok-lbl">Save</div>
</div>
<div class="eu-tiktok-action" data-act="copy" title="Copy URL">
<div class="eu-tiktok-icon">${ICONS.copy}</div>
<div class="eu-tiktok-lbl">Copy</div>
</div>
<div class="eu-tiktok-action eu-tiktok-action-mini" data-act="report" title="Report">
<div class="eu-tiktok-icon">${ICONS.flag}</div>
</div>
<div class="eu-tiktok-disc">
<img src="https://api.dicebear.com/7.x/initials/svg?seed=${avatarSeed}&backgroundColor=14151f" alt="">
</div>
</div>
${item.kind === 'video' ? '<div class="eu-tiktok-progress"><div class="eu-tiktok-progress-fill"></div></div>' : ''}
`;
// Actions — Erome-native buttons
el.querySelectorAll('.eu-tiktok-action').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const act = btn.dataset.act;
if (act === 'download') downloadSingle(item.url, getFilename(item.url));
if (act === 'copy') {
navigator.clipboard?.writeText(item.url)
.then(() => toast('URL copied!', 'success', 1400))
.catch(() => toast('Copy failed', 'error'));
}
if (act === 'like') {
btn.classList.toggle('liked');
if (btn.classList.contains('liked')) {
showHeartPop(el);
toast('Liked \u2665', 'success', 1200);
}
}
if (act === 'favorite') {
btn.classList.toggle('faved');
toast(btn.classList.contains('faved') ? 'Added to favorites \u2b50' : 'Removed from favorites', 'success', 1400);
}
if (act === 'comment') {
if (item.albumUrl) window.open(item.albumUrl + '#comments', '_blank');
else toast('Comments available on the full album page', 'info');
}
if (act === 'share') {
const shareUrl = item.albumUrl || item.url;
if (navigator.share) {
navigator.share({ title: `Erome \u00b7 @${username}`, url: shareUrl }).catch(() => {});
} else {
navigator.clipboard?.writeText(shareUrl)
.then(() => toast('Share link copied \ud83d\udce4', 'success', 1400))
.catch(() => toast('Copy failed', 'error'));
}
}
if (act === 'report') {
if (confirm('Open the full album page to report this content?') && item.albumUrl) {
window.open(item.albumUrl, '_blank');
}
}
});
});
// Double-tap to like (mobile gesture)
let lastTap = 0;
el.addEventListener('click', (e) => {
if (e.target.closest('.eu-tiktok-sidebar') || e.target.closest('.eu-tiktok-album')) return;
const now = Date.now();
if (now - lastTap < 350) {
// Double tap
const likeBtn = el.querySelector('.eu-tiktok-action[data-act="like"]');
if (likeBtn && !likeBtn.classList.contains('liked')) {
likeBtn.classList.add('liked');
}
showHeartPop(el);
} else {
// Single tap = play/pause for videos
if (item.kind === 'video') {
const video = el.querySelector('video');
const hint = el.querySelector('.eu-tiktok-play-hint');
if (video) {
if (video.paused) { video.play().catch(() => {}); hint.classList.remove('show'); }
else { video.pause(); hint.classList.add('show'); }
}
}
}
lastTap = now;
});
// Observe for autoplay
if (TT.io) TT.io.observe(el);
// Insert before loading card if present
const loadingCard = feed.querySelector('.eu-tiktok-loading');
if (loadingCard) feed.insertBefore(el, loadingCard);
else feed.appendChild(el);
// Video progress bar
if (item.kind === 'video') {
const video = el.querySelector('video');
const fill = el.querySelector('.eu-tiktok-progress-fill');
video?.addEventListener('timeupdate', () => {
if (video.duration) fill.style.width = `${(video.currentTime / video.duration) * 100}%`;
});
}
}
function showHeartPop(cardEl) {
const pop = cardEl.querySelector('.eu-tiktok-heart-pop');
if (!pop) return;
pop.classList.remove('animate');
void pop.offsetWidth;
pop.classList.add('animate');
}
function buildTikTokOverlay() {
if (document.getElementById('eu-tiktok')) return;
const overlay = document.createElement('div');
overlay.id = 'eu-tiktok';
overlay.className = 'eu-tiktok-overlay';
overlay.innerHTML = `
<div class="eu-phone">
<div class="eu-tiktok-splash" id="eu-tiktok-splash">
<img src="${LOGO_SPLASH}" alt="Erome" class="eu-tiktok-splash-logo">
<div class="eu-tiktok-splash-tag">Ultimate Premium</div>
<div class="eu-tiktok-splash-dots"><span></span><span></span><span></span></div>
</div>
<div class="eu-tiktok-topbar">
<div class="eu-tiktok-title">
<img src="${LOGO_ICON}" alt="Erome" class="eu-tiktok-brand-logo">
<span>For You</span>
</div>
<div class="eu-tiktok-controls">
<button class="eu-tiktok-tab" data-type="videos">Videos</button>
<button class="eu-tiktok-tab" data-type="photos">Photos</button>
<button class="eu-tiktok-tab" data-type="all">All</button>
<button class="eu-tiktok-close" title="Close">${ICONS.close}</button>
</div>
</div>
<div class="eu-tiktok-counter" id="eu-tiktok-counter">0 / 0</div>
<div class="eu-tiktok-feed" id="eu-tiktok-feed"></div>
</div>`;
document.body.appendChild(overlay);
overlay.querySelector('.eu-tiktok-close').addEventListener('click', closeTikTok);
overlay.addEventListener('click', (e) => {
// Click outside phone closes
if (e.target === overlay) closeTikTok();
});
overlay.querySelectorAll('.eu-tiktok-tab').forEach(tab => {
tab.addEventListener('click', () => {
STATE.tiktokType = tab.dataset.type;
updateTikTokTabs();
resetAndRenderFeed();
});
});
// Setup IntersectionObserver once
const feed = overlay.querySelector('#eu-tiktok-feed');
TT.io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const card = entry.target;
const videoEl = card.querySelector('video');
const idx = parseInt(card.dataset.idx);
if (entry.isIntersecting && entry.intersectionRatio > 0.65) {
STATE.tiktokIndex = idx;
updateTikTokCounter();
if (videoEl) {
videoEl.muted = false;
videoEl.play().catch(() => {
// Autoplay with sound blocked — fall back to muted
videoEl.muted = true;
videoEl.play().catch(() => {});
});
}
// Preload next album when nearing the end
if (idx >= TT.items.length - 3) {
preloadNextAlbum();
}
} else if (videoEl) {
videoEl.pause();
}
});
}, { root: feed, threshold: [0, 0.65, 0.9] });
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (!STATE.tiktokMode) return;
if (e.key === 'Escape') closeTikTok();
else if (e.key === 'ArrowDown' || e.key === 'j') { scrollTikTok(1); e.preventDefault(); }
else if (e.key === 'ArrowUp' || e.key === 'k') { scrollTikTok(-1); e.preventDefault(); }
else if (e.key === ' ') {
const card = getCurrentCard();
const video = card?.querySelector('video');
if (video) {
if (video.paused) video.play(); else video.pause();
e.preventDefault();
}
}
else if (e.key === 'd') {
const item = getCurrentItem();
if (item) downloadSingle(item.url, getFilename(item.url));
}
else if (e.key === 'l') {
const card = getCurrentCard();
card?.querySelector('.eu-tiktok-action[data-act="like"]')?.click();
}
});
}
function updateTikTokTabs() {
document.querySelectorAll('.eu-tiktok-tab').forEach(t => {
t.classList.toggle('active', t.dataset.type === STATE.tiktokType);
});
}
function getCurrentCard() {
const feed = document.getElementById('eu-tiktok-feed');
if (!feed) return null;
const cards = feed.querySelectorAll('.eu-tiktok-item');
return cards[STATE.tiktokIndex] || cards[0] || null;
}
function getCurrentItem() {
return getCurrentCard()?._euData || null;
}
function scrollTikTok(direction) {
const feed = document.getElementById('eu-tiktok-feed');
if (!feed) return;
const cards = feed.querySelectorAll('.eu-tiktok-item');
const next = Math.max(0, Math.min(cards.length - 1, STATE.tiktokIndex + direction));
cards[next]?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function resetAndRenderFeed() {
const feed = document.getElementById('eu-tiktok-feed');
if (!feed) return;
// Reset state
TT.items = [];
TT.seenUrls = new Set();
TT.albumQueue = [];
TT.albumSeen = new Set();
if (IS_ALBUM_PAGE) TT.albumSeen.add(location.href);
STATE.tiktokIndex = 0;
feed.innerHTML = '';
// Collect from current page
const pageItems = collectCurrentPageItems(STATE.tiktokType);
TT.items = pageItems;
if (pageItems.length === 0) {
feed.innerHTML = `<div class="eu-tiktok-empty">
<div style="font-size:44px;margin-bottom:14px;">📭</div>
<div style="font-weight:600;margin-bottom:4px;">No ${STATE.tiktokType} on this page</div>
<div style="font-size:11px;opacity:.6;">Try another tab above, or open an album.</div>
</div>`;
const c = document.getElementById('eu-tiktok-counter');
if (c) c.textContent = '0 / 0';
return;
}
pageItems.forEach((item, idx) => appendTikTokCard(item, idx));
updateTikTokCounter();
feed.scrollTop = 0;
// Build queue of related albums for infinite background loading
buildNextAlbumQueue();
// Preload first background album after a short delay
if (IS_ALBUM_PAGE && TT.albumQueue.length > 0) {
setTimeout(() => preloadNextAlbum(), 1500);
}
}
function openTikTok(type) {
buildTikTokOverlay();
if (type) STATE.tiktokType = type;
updateTikTokTabs();
STATE.tiktokMode = true;
document.getElementById('eu-tiktok').classList.add('open');
document.body.style.overflow = 'hidden';
resetAndRenderFeed();
toast(`TikTok mode — swipe up/down, tap to pause, double-tap to like`, 'info', 3500);
}
function closeTikTok() {
STATE.tiktokMode = false;
const o = document.getElementById('eu-tiktok');
if (o) {
o.classList.remove('open');
// Pause all videos
o.querySelectorAll('video').forEach(v => v.pause());
}
document.body.style.overflow = '';
}
/* ============================================================
* FLOATING ACTION BUTTON (FAB)
* ============================================================ */
function buildFAB() {
if (document.querySelector('.eu-fab')) return;
const fab = document.createElement('button');
fab.className = 'eu-fab';
fab.title = 'Bulk Download';
fab.innerHTML = `${ICONS.download}<span class="eu-fab-badge" id="eu-fab-badge">0</span>`;
fab.addEventListener('click', openBulkModal);
document.body.appendChild(fab);
// TikTok-mode FAB on album pages — uses the Erome logo
if (IS_ALBUM_PAGE && !document.querySelector('.eu-fab-tiktok')) {
const tt = document.createElement('button');
tt.className = 'eu-fab eu-fab-tiktok';
tt.title = 'TikTok-style Feed';
tt.style.bottom = '92px';
tt.style.background = 'linear-gradient(135deg, #ff0050 0%, #8a5acc 50%, #00f2ea 100%)';
tt.innerHTML = `<img src="${LOGO_ICON}" alt="Erome" style="width:32px;height:32px;border-radius:50%;object-fit:cover;box-shadow:0 0 8px rgba(0,0,0,0.4);">`;
tt.addEventListener('click', () => openTikTok(STATE.tiktokType));
document.body.appendChild(tt);
}
}
function updateFabBadge() {
const badge = document.getElementById('eu-fab-badge');
if (!badge) return;
const n = collectAllMediaUrls().length;
badge.textContent = n;
badge.style.display = n > 0 ? 'flex' : 'none';
}
/* ============================================================
* HUB MENU (Twitter-style popup cards in navbar)
* ============================================================ */
function buildHubMenu(id, iconKey, items, title = '') {
if (document.getElementById(id)) return null;
const hub = document.createElement('div');
hub.id = id;
hub.className = 'eu-hub';
hub.innerHTML = `
<div class="eu-hub-btn" title="${title}">${ICONS[iconKey] || ICONS.settings}</div>
<ul class="eu-hub-menu"></ul>`;
const menu = hub.querySelector('.eu-hub-menu');
items.forEach(item => {
const li = document.createElement('li');
li.className = 'eu-hub-item';
li.innerHTML = `${ICONS[item.icon] || ICONS.settings}<span>${item.label}</span>`;
li.addEventListener('click', (e) => {
e.stopPropagation();
item.action?.();
hub.classList.remove('open');
});
menu.appendChild(li);
});
hub.querySelector('.eu-hub-btn').addEventListener('click', (e) => {
e.stopPropagation();
document.querySelectorAll('.eu-hub.open').forEach(h => { if (h !== hub) h.classList.remove('open'); });
hub.classList.toggle('open');
});
return hub;
}
function installHubs() {
const navToggle = document.querySelector('.navbar-toggle');
if (!navToggle) return;
const target = navToggle.parentNode;
const ref = navToggle.nextSibling;
// Privacy hub
const privacyItems = [
{ label: 'Toggle NSFW Blur', icon: 'eye', action: toggleNSFW },
{ label: 'Toggle Hidden Filter', icon: 'clock', action: () => {
const s = document.querySelector('.eu-hidden-slider');
if (s) s.classList.toggle('visible');
toast('Hidden filter ' + (s?.classList.contains('visible') ? 'on' : 'off'), 'info');
} }
];
const privacyHub = buildHubMenu('eu-privacy-hub', 'eye', privacyItems, 'Privacy');
if (privacyHub) target.insertBefore(privacyHub, ref);
// View hub (listing pages)
if (!IS_ALBUM_PAGE) {
const viewItems = [
{ label: 'Sort by Views', icon: 'sort', action: () => sortAlbums('views') },
{ label: 'Sort by Video Count', icon: 'video', action: () => sortAlbums('videos') },
{ label: 'Sort by Photo Count', icon: 'photo', action: () => sortAlbums('photos') },
{ label: 'Toggle Sort Direction', icon: 'sort', action: () => {
STATE.sortAscending = !STATE.sortAscending;
sortAlbums(STATE.sortMode);
} },
{ label: 'Video-Only Albums', icon: 'video', action: toggleVideoOnly }
];
const viewHub = buildHubMenu('eu-view-hub', 'sort', viewItems, 'View & Sort');
if (viewHub) target.insertBefore(viewHub, ref);
}
// Album hub (album pages)
if (IS_ALBUM_PAGE) {
const albumItems = [
{ label: 'TikTok Mode — Videos', icon: 'video', action: () => openTikTok('videos') },
{ label: 'TikTok Mode — Photos', icon: 'photo', action: () => openTikTok('photos') },
{ label: 'TikTok Mode — All', icon: 'cinema', action: () => openTikTok('all') },
{ label: 'Bulk Download…', icon: 'download', action: openBulkModal },
{ label: 'Toggle Download Buttons', icon: 'upload', action: toggleDownloadButtons },
{ label: 'Toggle Photos', icon: 'photo', action: togglePhotos },
{ label: 'Toggle Videos', icon: 'video', action: toggleVideos },
{ label: 'Cinema Mode', icon: 'cinema', action: toggleCinema }
];
const albumHub = buildHubMenu('eu-album-hub', 'settings', albumItems, 'Album Tools');
if (albumHub) target.insertBefore(albumHub, ref);
}
// Close hubs on outside click
document.addEventListener('click', () => {
document.querySelectorAll('.eu-hub.open').forEach(h => h.classList.remove('open'));
});
}
/* ============================================================
* DISCLAIMER / ACCOUNT BYPASS
* ============================================================ */
function bypass() {
const disc = document.getElementById('disclaimer');
if (disc) {
disc.remove();
document.body.style.overflow = 'visible';
try {
if (typeof $ !== 'undefined') $.ajax({ type: 'POST', url: '/user/disclaimer', async: true });
else fetch('/user/disclaimer', { method: 'POST' });
} catch (e) {}
}
const needAcc = document.getElementById('needAccount');
if (needAcc) needAcc.remove();
}
/* ============================================================
* INFINITE SCROLL (listing pages)
* ============================================================ */
function installInfiniteScroll() {
if (IS_ALBUM_PAGE) return;
if (!document.querySelector('.pagination')) return;
if (typeof $ === 'undefined' || !$.fn.infiniteScroll) return;
const url = new URL(window.location.href);
let nextPage = parseInt(url.searchParams.get('page')) || 2;
const limit = parseInt($('.pagination li:last-child').prev().text()) || 50;
const nextPageUrl = () => {
url.searchParams.set('page', nextPage);
return url.href;
};
try {
const infinite = $('#page').infiniteScroll({
path: nextPageUrl,
append: '.page-content',
scrollThreshold: 800
});
$('#page').on('append.infiniteScroll', () => {
nextPage++;
if (nextPage > limit) infinite.infiniteScroll('destroy');
refreshAll();
});
} catch (e) { console.warn('[EU] Infinite scroll not available:', e); }
}
/* ============================================================
* PERIODIC REFRESH — runs all decorators
* ============================================================ */
function refreshAll() {
attachDownloadButtons();
decorateAlbumCounts();
decorateVideoDurations();
applyNSFW();
applyVideoOnly();
applyHiddenFilter();
updateFabBadge();
enhancePlayers();
}
/* ============================================================
* INITIALIZATION
* ============================================================ */
function init() {
console.log(`[${CONFIG.brand}] v${CONFIG.version} initializing…`);
bypass();
installHubs();
buildFAB();
createHiddenSlider();
refreshAll();
loadAlbumLikes();
installInfiniteScroll();
// Periodic refresh for dynamic content
setInterval(refreshAll, 2000);
// MutationObserver for real-time updates
const mo = new MutationObserver(() => {
clearTimeout(window._euDebounce);
window._euDebounce = setTimeout(refreshAll, 300);
});
mo.observe(document.body, { childList: true, subtree: true });
// Welcome toast
setTimeout(() => {
const n = collectAllMediaUrls().length;
toast(`${CONFIG.brand} ready — ${n} media items detected`, 'success', 3500);
}, 1200);
console.log(`[${CONFIG.brand}] ready`);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('load', () => { setTimeout(refreshAll, 1000); });
// Expose API for debugging
window.eromeUltimate = {
state: STATE,
config: CONFIG,
collectAllMediaUrls,
openBulkModal,
toast,
refreshAll
};
})();